Compare commits

..

18 Commits

Author SHA1 Message Date
Andrew Murray
b627fb97c7
Merge branch 'main' into imaging-cleanup 2024-12-29 20:12:50 +11:00
Alexander Karpinsky
001ee3147d
Update comment
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2024-10-07 19:11:22 +04:00
Alexander Karpinsky
5e07d85161 Update comment [ci skip]
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
2024-10-07 18:58:01 +04:00
Aleksandr Karpinskii
37b2a1bbae Cleanup exceptions handling
Remove unused module
Remove ImagingError_Clear alias
Do not set PyExc_TypeError after PySequence_Fast
Remove ImagingError_OSError alias
Use PyErr_Format when possible
2024-10-07 18:58:01 +04:00
Aleksandr Karpinskii
3a16a350cc Fix Imaging_Type check in _convert 2024-10-07 18:58:01 +04:00
Aleksandr Karpinskii
50dd94664b Correct types of _imaging methods
Move new_block method to _imaging
remove _imaging.convert method
2024-10-07 18:58:01 +04:00
Aleksandr Karpinskii
8ba076ae86 Remove unused declaration 2024-10-07 18:55:09 +04:00
Aleksandr Karpinskii
24daafa2eb Use Py_RETURN_NONE macro when possible 2024-10-07 18:55:09 +04:00
Aleksandr Karpinskii
310f625e60 Remove obsolete PyImaging_CheckBuffer and PyImaging_GetBuffer 2024-10-07 18:55:09 +04:00
Aleksandr Karpinskii
517c3e1c7f import global libs correctly 2024-10-07 18:55:09 +04:00
Aleksandr Karpinskii
bd03721816 Remove add-imaging-libs option
# Conflicts:
#	setup.py
2024-10-07 18:55:09 +04:00
Aleksandr Karpinskii
039869fe77 Move import math.h to ImagingPlatform 2024-10-07 18:55:09 +04:00
Aleksandr Karpinskii
ff7c29ced9 Move include Python.h to ImagingPlatform 2024-10-07 18:55:08 +04:00
Aleksandr Karpinskii
34488d1d4e Drop support of not C99+ compilers (including old MSVC) 2024-10-07 18:55:08 +04:00
Aleksandr Karpinskii
2a7c9670c1 Cleanup Jpeg.h undefs 2024-10-07 18:54:27 +04:00
Aleksandr Karpinskii
c46ff64052 Remove tiff extra guards 2024-10-07 18:54:27 +04:00
Aleksandr Karpinskii
f4accfe336 Rename ImPlarform to ImagingPlatform 2024-10-07 18:54:27 +04:00
Aleksandr Karpinskii
7b3179161f MIN and MAX macros in ImagingUtils 2024-10-07 18:54:27 +04:00
577 changed files with 9856 additions and 32613 deletions

99
.appveyor.yml Normal file
View File

@ -0,0 +1,99 @@
skip_commits:
files:
- ".github/**/*"
- ".gitmodules"
- "docs/**/*"
- "wheels/**/*"
version: '{build}'
clone_folder: c:\pillow
init:
- ECHO %PYTHON%
#- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
# Uncomment previous line to get RDP access during the build.
environment:
COVERAGE_CORE: sysmon
EXECUTABLE: python.exe
TEST_OPTIONS:
DEPLOY: YES
matrix:
- PYTHON: C:/Python313
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python39-x64
ARCHITECTURE: AMD64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
install:
- '%PYTHON%\%EXECUTABLE% --version'
- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
- 7z x pillow-test-images.zip -oc:\
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
- 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.4.0
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.04.0\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |
c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
c:\pillow\winbuild\build\build_dep_all.cmd
$host.SetShouldExit(0)
- path C:\pillow\winbuild\build\bin;%PATH%
build_script:
- cd c:\pillow
- winbuild\build\build_env.cmd
- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .'
- '%PYTHON%\%EXECUTABLE% selftest.py --installed'
test_script:
- cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- path %PYTHON%;%PATH%
- .ci\test.cmd
after_test:
- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe
- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor
matrix:
fast_finish: true
cache:
- '%LOCALAPPDATA%\pip\Cache'
artifacts:
- path: pillow\*.egg
name: egg
- path: pillow\*.whl
name: wheel
before_deploy:
- cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .'
- ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
deploy:
provider: S3
region: us-west-2
access_key_id: AKIAIRAXC62ZNTVQJMOQ
secret_access_key:
secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi
bucket: pillow-nightly
folder: win/$(APPVEYOR_BUILD_NUMBER)/
artifact: /.*egg|wheel/
on:
APPVEYOR_REPO_NAME: python-pillow/Pillow
branch: main
deploy: YES
# Uncomment the following lines to get RDP access after the build/test and block for
# up to the timeout limit (~1hr)
#
#on_finish:
#- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))

View File

@ -2,4 +2,8 @@
# gather the coverage data # gather the coverage data
python3 -m pip install coverage python3 -m pip install coverage
python3 -m coverage xml if [[ $MATRIX_DOCKER ]]; then
python3 -m coverage xml --ignore-errors
else
python3 -m coverage xml
fi

View File

@ -3,5 +3,8 @@
set -e set -e
python3 -m coverage erase python3 -m coverage erase
if [ $(uname) == "Darwin" ]; then
export CPPFLAGS="-I/usr/local/miniconda/include";
fi
make clean make clean
make install-coverage make install-coverage

View File

@ -2,25 +2,29 @@
aptget_update() aptget_update()
{ {
if [ -n "$1" ]; then if [ ! -z $1 ]; then
echo "" echo ""
echo "Retrying apt-get update..." echo "Retrying apt-get update..."
echo "" echo ""
fi fi
output=$(sudo apt-get update 2>&1) output=`sudo apt-get update 2>&1`
echo "$output" echo "$output"
if [[ $output == *[WE]:\ * ]]; then if [[ $output == *[WE]:\ * ]]; then
return 1 return 1
fi fi
} }
aptget_update || aptget_update retry || aptget_update retry if [[ $(uname) != CYGWIN* ]]; then
aptget_update || aptget_update retry || aptget_update retry
fi
set -e set -e
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ if [[ $(uname) != CYGWIN* ]]; then
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
sway wl-clipboard libopenblas-dev nasm cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev
fi
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel python3 -m pip install --upgrade wheel
@ -32,28 +36,34 @@ python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
# optional test dependencies, only install if there's a binary package.
python3 -m pip install --only-binary=:all: numpy || true
python3 -m pip install --only-binary=:all: pyarrow || true
# PyQt6 doesn't support PyPy3 if [[ $(uname) != CYGWIN* ]]; then
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then python3 -m pip install numpy
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
# pyqt6 doesn't yet support free-threading; only install if a wheel is available # PyQt6 doesn't support PyPy3
python3 -m pip install --only-binary=:all: pyqt6 || true 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
fi
# Pyroma uses non-isolated build and fails with old setuptools
if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then
# To match pyproject.toml
python3 -m pip install "setuptools>=67.8"
fi
# webp
pushd depends && ./install_webp.sh && popd
# libimagequant
pushd depends && ./install_imagequant.sh && popd
# raqm
pushd depends && ./install_raqm.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd
else
cd depends && ./install_extra_test_images.sh && cd ..
fi fi
# webp
pushd depends && ./install_webp.sh && popd
# libimagequant
pushd depends && ./install_imagequant.sh && popd
# raqm
pushd depends && sudo ./install_raqm.sh && popd
# libavif
pushd depends && ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -1 +1 @@
cibuildwheel==3.4.1 cibuildwheel==2.22.0

View File

@ -1,14 +1,11 @@
mypy==1.20.2 mypy==1.14.0
arro3-compute
arro3-core
IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6 IceSpringPySideStubs-PySide6
ipython ipython
numpy numpy
packaging packaging
pyarrow-stubs
pybind11
pytest pytest
sphinx
types-atheris types-atheris
types-defusedxml types-defusedxml
types-olefile types-olefile

View File

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

View File

@ -1,3 +1,3 @@
python.exe -c "from PIL import Image" python.exe -c "from PIL import Image"
IF ERRORLEVEL 1 EXIT /B IF ERRORLEVEL 1 EXIT /B
python.exe -bb -m pytest -vv -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests python.exe -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests

View File

@ -4,4 +4,4 @@ set -e
python3 -c "from PIL import Image" python3 -c "from PIL import Image"
python3 -bb -m pytest -vv -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE

View File

@ -1,6 +1,5 @@
# A clang-format style that approximates Python's PEP 7 # A clang-format style that approximates Python's PEP 7
# Useful for IDE integration # Useful for IDE integration
Language: C
BasedOnStyle: Google BasedOnStyle: Google
AlwaysBreakAfterReturnType: All AlwaysBreakAfterReturnType: All
AllowShortIfStatementsOnASingleLine: false AllowShortIfStatementsOnASingleLine: false
@ -12,26 +11,7 @@ ColumnLimit: 88
DerivePointerAlignment: false DerivePointerAlignment: false
IndentGotoLabels: false IndentGotoLabels: false
IndentWidth: 4 IndentWidth: 4
PointerAlignment: Right
ReflowComments: true
SortIncludes: false
SpaceBeforeParens: ControlStatements
SpacesInParentheses: false
TabWidth: 4
UseTab: Never
---
Language: Cpp Language: Cpp
BasedOnStyle: Google
AlwaysBreakAfterReturnType: All
AllowShortIfStatementsOnASingleLine: false
AlignAfterOpenBracket: BlockIndent
BinPackArguments: false
BinPackParameters: false
BreakBeforeBraces: Attach
ColumnLimit: 88
DerivePointerAlignment: false
IndentGotoLabels: false
IndentWidth: 4
PointerAlignment: Right PointerAlignment: Right
ReflowComments: true ReflowComments: true
SortIncludes: false SortIncludes: false

View File

@ -18,5 +18,6 @@ exclude_also =
[run] [run]
omit = omit =
checks/*.py Tests/32bit_segfault_check.py
Tests/check_*.py
Tests/createfontdatachunk.py Tests/createfontdatachunk.py

View File

@ -9,7 +9,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Fork the Pillow repository. - Fork the Pillow repository.
- Create a branch from `main`. - Create a branch from `main`.
- Develop bug fixes, features, tests, etc. - Develop bug fixes, features, tests, etc.
- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests. - Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
- Create a pull request to pull the changes from your branch to the Pillow `main`. - Create a pull request to pull the changes from your branch to the Pillow `main`.
### Guidelines ### Guidelines
@ -17,7 +17,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Separate code commits from reformatting commits. - Separate code commits from reformatting commits.
- Provide tests for any newly added code. - Provide tests for any newly added code.
- Follow PEP 8. - Follow PEP 8.
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running extra tests. - When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor.
- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. - Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests.
## Reporting Issues ## Reporting Issues

3
.github/FUNDING.yml vendored
View File

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

View File

@ -1,424 +0,0 @@
# Incident Response Plan — Pillow
This document describes how the Pillow maintainers detect, triage, fix, communicate, and
learn from security incidents. It supplements the existing [Security Policy](SECURITY.md)
and [Release Checklist](../RELEASING.md).
---
## 1. Preparation
Maintaining readiness before an incident occurs reduces response time and errors under pressure.
### 1.1 Version Support Matrix
Security fixes are applied to the **latest stable release only**. Users on older versions
are expected to upgrade. Reporters should assume only the latest release will receive a patch.
| Branch | Status |
|---|---|
| `main` / latest stable | ✅ Security fixes applied |
| All older releases | ❌ No security support — please upgrade |
### 1.2 Team Readiness
The four members of the Pillow core team are in regular contact and share collective
responsibility for incident response. Any core team member may act as Incident Lead.
Contact details are known to all team members.
### 1.3 Readiness Review
At each quarterly release, maintainers should re-read this document and update any stale content.
---
## 2. Scope
This plan covers:
| Incident type | Examples |
|---|---|
| Vulnerability in Pillow's own Python or C code | Buffer overflow in an image decoder, integer overflow in `ImagingNew` |
| Vulnerability in a bundled or wheel-shipped C library | libjpeg, libwebp, libtiff, libpng, openjpeg, libavif |
| Supply-chain compromise | Malicious commit, stolen maintainer credentials, tampered PyPI wheel |
| CI/CD or infrastructure compromise | GitHub Actions secret leak, Codecov breach, PyPI token exposure |
| Critical non-security regression | Data-loss bug shipped in a release, crash on all supported platforms |
---
## 3. Definitions
| Term | Meaning |
|---|---|
| **Incident** | Any event that compromises or threatens the confidentiality, integrity, or availability of Pillow's code, release artifacts, or infrastructure. |
| **Vulnerability** | A security flaw in Pillow or a bundled library that can be exploited by a crafted image or API call. |
| **Incident Lead** | The maintainer who owns coordination of the response from triage to closure. |
| **Embargo** | A period during which fix details are kept private to allow coordinated patching before public disclosure. |
| **Yank** | A PyPI action that keeps a release downloadable by pinned users but removes it from default `pip install` resolution. |
| **CVE** | Common Vulnerabilities and Exposures — a public identifier assigned to a specific vulnerability. |
| **CNA** | CVE Numbering Authority — GitHub is a CNA and can assign CVEs directly through the advisory workflow. |
---
## 4. Roles
| Role | Responsibility |
|---|---|
| **Incident Lead** | First maintainer to triage the report. Owns the incident until resolution. |
| **Patch Owner** | Writes and tests the fix (may be the same person as Incident Lead). |
| **Release Manager** | Cuts the point release following [RELEASING.md](../RELEASING.md). |
| **Communications Owner** | Drafts the GitHub Security Advisory, announces on Mastodon, notifies distros. |
| **Tidelift Contact** | For reports that arrive via Tidelift, coordinate through the Tidelift security portal. |
One person may fill multiple roles.
---
## 5. Severity Classification
Use the [CVSS 4.0](https://www.first.org/cvss/v4.0/specification-document) base score as
a guide, mapped to the following levels:
| Severity | CVSS | Definition | Target Response SLA |
|---|---|---|---|
| **Critical** | 9.0 10.0 | Remote code execution, arbitrary write, or complete integrity/confidentiality loss achievable by opening a crafted image | Best effort; embargoed release where possible |
| **High** | 7.0 8.9 | Heap/stack buffer overflow, use-after-free, or significant information disclosure | Best effort |
| **Medium** | 4.0 6.9 | Denial of service via crafted image, out-of-bounds read, limited info disclosure | Next scheduled quarterly release, or earlier point release if needed |
| **Low** | 0.1 3.9 | Minor information disclosure, unlikely to be exploitable in practice | Next quarterly release |
Supply-chain and CI/CD incidents are always treated as **Critical** regardless of CVSS.
> **Note:** These are good-faith targets for a small volunteer maintainer team, not contractual SLAs. Public safety and transparency will always be prioritised, even when timing varies.
---
## 6. Detection Sources
Vulnerabilities and incidents may be reported or discovered through:
1. **GitHub private security advisory** — preferred channel; see [SECURITY.md](SECURITY.md)
2. **Tidelift security contact**<https://tidelift.com/docs/security>
3. **External researcher / coordinated disclosure** — e.g. Google Project Zero, vendor PSIRT
4. **Automated scanning** — Dependabot, GitHub code-scanning (CodeQL), CI fuzzing
5. **Distro security teams** — Debian, Red Hat, Ubuntu, Alpine may report upstream
6. **User bug report** — public issue (reassess if it has security implications and convert to a private advisory if needed)
---
## 7. Response Process
### 7.1 Triage (all severities)
1. **Acknowledge receipt** to the reporter within **72 hours** using the template in
[Appendix A](#appendix-a-communication-templates). Ask the reporter:
- How they would like to be credited (name, handle, or anonymous)
- Whether they intend to publish their own advisory, and if so, their preferred timeline
- Thank them explicitly — reporters do the project a favour by disclosing privately.
2. Reproduce the issue. If the report is invalid, close it and notify the reporter.
3. Assign a severity level ([Section 5: Severity Classification](#5-severity-classification)).
4. If the GitHub Security Advisory was not created by the reporter, create one now and keep
it **private** until the fix is released. Add the reporter as a collaborator if they wish
to be involved.
5. **Request a CVE** through the GitHub Security Advisory workflow (GitHub is a CVE
Numbering Authority — no separate MITRE form required). The CVE is reserved privately
and published automatically when the advisory goes public.
6. **Escalation** — Escalate beyond the core maintainer team if any of the following apply:
- The fix requires changes to CPython or a dependency outside Pillow's control → contact the relevant upstream immediately
- A legal concern arises (e.g. GDPR-reportable data exposure) → contact the project's legal/fiscal sponsor
- The Incident Lead is unreachable for > 24 hours on a Critical issue → any other maintainer may assume the role
### 7.2 Fix Development
1. Develop the fix in a **private fork** or directly in the private security advisory
workspace on GitHub. Do **not** push to a public branch before the embargo lifts.
2. Write a regression test that fails before the fix and passes after.
3. Review the patch with at least one other maintainer.
### 7.3 Standard (Non-Embargoed) Release
For Medium and Low severity, or when no distro pre-notification is needed:
1. Merge the fix to `main`, then cherry-pick to all affected release branches
(see [RELEASING.md — Point release](../RELEASING.md)).
2. Amend commit messages to include the CVE identifier.
3. Follow the [Point release](../RELEASING.md#point-release) process in RELEASING.md to
tag, push, and confirm wheels are live on PyPI.
4. Publish the GitHub Security Advisory (this simultaneously publishes the CVE).
### 7.4 Embargoed Release
For Critical and High severity where distro pre-notification improves user safety:
1. Prepare patches against all affected release branches and test locally.
2. Agree on an **embargo date** with the reporter (typically 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\>

View File

@ -1,46 +0,0 @@
---
name: "Maintainers only: Release"
about: For maintainers to schedule a quarterly release
labels: Release
---
## Main release
Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `main` branch.
* [ ] Add release notes e.g. https://github.com/python-pillow/Pillow/pull/8885
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch.
* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
* [ ] Create branch and tag for release e.g.:
```bash
git branch [[MAJOR.MINOR]].x
git tag [[MAJOR.MINOR]].0
git push --tags
```
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) has passed, including the "Upload release to PyPI" job. This will have been triggered by the new tag.
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases).
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
```bash
git push --all
```
## Publicize release
* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321
## Documentation
* [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes
## Docker images
* [ ] Update Pillow in the Docker Images repository
```bash
git clone https://github.com/python-pillow/docker-images
cd docker-images
./update-pillow-tag.sh [[release tag]]
```

20
.github/SECURITY.md vendored
View File

@ -1,21 +1,5 @@
# Security policy # Security policy
## Reporting a vulnerability To report sensitive vulnerability information, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
To report sensitive vulnerability information, report it [privately on GitHub](https://github.com/python-pillow/Pillow/security/advisories/new). 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.
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.

View File

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

View File

@ -1,19 +0,0 @@
{
"brotli": "1.2.0",
"bzip2": "1.0.8",
"freetype": "2.14.3",
"fribidi": "1.0.16",
"harfbuzz": "14.2.0",
"jpegturbo": "3.1.4.1",
"lcms2": "2.19",
"libavif": "1.4.1",
"libimagequant": "4.4.1",
"libpng": "1.6.58",
"libwebp": "1.6.0",
"libxcb": "1.17.0",
"openjpeg": "2.5.4",
"tiff": "4.7.1",
"xz": "5.8.3",
"zlib-ng": "2.3.3",
"zstd": "1.5.7"
}

View File

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

2
.github/mergify.yml vendored
View File

@ -8,6 +8,8 @@ pull_request_rules:
- status-success=Docker Test Successful - status-success=Docker Test Successful
- status-success=Windows Test Successful - status-success=Windows Test Successful
- status-success=MinGW - status-success=MinGW
- status-success=Cygwin Test Successful
- status-success=continuous-integration/appveyor/pr
actions: actions:
merge: merge:
method: merge method: merge

166
.github/renovate.json vendored
View File

@ -6,170 +6,16 @@
"labels": [ "labels": [
"Dependency" "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": [ "packageRules": [
{ {
"groupName": "github-actions", "groupName": "github-actions",
"matchManagers": ["github-actions"], "matchManagers": [
"github-actions"
],
"separateMajorMinor": false "separateMajorMinor": false
} }
],
"schedule": [
"on the 3rd day of the month"
] ]
} }

View File

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

View File

@ -4,14 +4,17 @@ on:
push: push:
branches: branches:
- "**" - "**"
paths: &paths paths:
- ".github/dependencies.json"
- ".github/workflows/cifuzz.yml" - ".github/workflows/cifuzz.yml"
- ".github/workflows/wheels-dependencies.sh" - ".github/workflows/wheels-dependencies.sh"
- "**.c" - "**.c"
- "**.h" - "**.h"
pull_request: pull_request:
paths: *paths paths:
- ".github/workflows/cifuzz.yml"
- ".github/workflows/wheels-dependencies.sh"
- "**.c"
- "**.h"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -21,36 +24,33 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs: jobs:
Fuzzing: Fuzzing:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Build Fuzzers - name: Build Fuzzers
id: build id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@d87225267726cf7ce1a3e17cf103c5ac943c4f05 # master uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
with: with:
oss-fuzz-project-name: 'pillow' oss-fuzz-project-name: 'pillow'
language: python language: python
dry-run: false dry-run: false
- name: Run Fuzzers - name: Run Fuzzers
id: run id: run
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@d87225267726cf7ce1a3e17cf103c5ac943c4f05 # master uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with: with:
oss-fuzz-project-name: 'pillow' oss-fuzz-project-name: 'pillow'
fuzz-seconds: 600 fuzz-seconds: 600
language: python language: python
dry-run: false dry-run: false
- name: Upload New Crash - name: Upload New Crash
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@v4
if: failure() && steps.build.outcome == 'success' if: failure() && steps.build.outcome == 'success'
with: with:
name: artifacts name: artifacts
path: ./out/artifacts path: ./out/artifacts
- name: Upload Legacy Crash - name: Upload Legacy Crash
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@v4
if: steps.run.outcome == 'success' if: steps.run.outcome == 'success'
with: with:
name: crash name: crash

View File

@ -4,12 +4,15 @@ on:
push: push:
branches: branches:
- "**" - "**"
paths: &paths paths:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- "docs/**" - "docs/**"
- "src/PIL/**" - "src/PIL/**"
pull_request: pull_request:
paths: *paths paths:
- ".github/workflows/docs.yml"
- "docs/**"
- "src/PIL/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -29,12 +32,12 @@ jobs:
name: Docs name: Docs
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
cache: pip cache: pip
@ -45,35 +48,19 @@ jobs:
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py 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 - name: Cache libimagequant
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@v4
id: cache-libimagequant id: cache-libimagequant
with: with:
path: ~/cache-libimagequant path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} 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 - name: Install Linux dependencies
run: | run: |
.ci/install.sh .ci/install.sh
env: env:
GHA_PYTHON_VERSION: "3.x" 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_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
- name: Build - name: Build
run: | run: |

View File

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

View File

@ -2,7 +2,23 @@
set -e set -e
brew bundle --file=.github/workflows/Brewfile if [[ "$ImageOS" == "macos13" ]]; then
brew uninstall gradle maven
fi
brew install \
freetype \
ghostscript \
jpeg-turbo \
libimagequant \
libtiff \
little-cms2 \
openjpeg \
webp
if [[ "$ImageOS" == "macos13" ]]; then
brew install --ignore-dependencies libraqm
else
brew install libraqm
fi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
python3 -m pip install coverage python3 -m pip install coverage
@ -13,12 +29,7 @@ python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
# optional test dependencies, only install if there's a binary package. python3 -m pip install numpy
python3 -m pip install --only-binary=:all: numpy || true
python3 -m pip install --only-binary=:all: pyarrow || true
# libavif
pushd depends && ./install_libavif.sh && popd
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -14,9 +14,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs: jobs:
update_release_draft: update_release_draft:
permissions: permissions:
@ -26,6 +23,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# Drafts your next release notes as pull requests are merged into "main" # Drafts your next release notes as pull requests are merged into "main"
- uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0 - uses: release-drafter/release-drafter@v6
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -12,12 +12,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs: jobs:
stale: stale:
if: github.event.repository.fork == false if: github.repository_owner == 'python-pillow'
permissions: permissions:
issues: write issues: write
@ -25,7 +22,7 @@ jobs:
steps: steps:
- name: "Check issues" - name: "Check issues"
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 uses: actions/stale@v9
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action" only-labels: "Awaiting OP Action"

154
.github/workflows/test-cygwin.yml vendored Normal file
View File

@ -0,0 +1,154 @@
name: Test Cygwin
on:
push:
branches:
- "**"
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/**"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs:
build:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
python-minor-version: [9]
timeout-minutes: 40
name: Python 3.${{ matrix.python-minor-version }}
steps:
- name: Fix line endings
run: |
git config --global core.autocrlf input
- name: Checkout Pillow
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Cygwin
uses: cygwin/cygwin-install-action@v4
with:
packages: >
gcc-g++
ghostscript
git
ImageMagick
jpeg
libfreetype-devel
libimagequant-devel
libjpeg-devel
liblapack-devel
liblcms2-devel
libopenjp2-devel
libraqm-devel
libtiff-devel
libwebp-devel
libxcb-devel
libxcb-xinerama0
make
netpbm
perl
python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-ipython
python3${{ matrix.python-minor-version }}-numpy
python3${{ matrix.python-minor-version }}-sip
python3${{ matrix.python-minor-version }}-tkinter
wget
xorg-server-extra
zlib-devel
- name: Add Lapack to PATH
uses: egor-tensin/cleanup-path@v4
with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
- name: Select Python version
run: |
ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
- name: pip cache
uses: actions/cache@v4
with:
path: 'C:\cygwin\home\runneradmin\.cache\pip'
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }}
restore-keys: |
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
- name: Build system information
run: |
dash.exe -c "python3 .github/workflows/system-info.py"
- name: Install dependencies
run: |
bash.exe .ci/install.sh
- name: Build
shell: bash.exe -eo pipefail -o igncr "{0}"
run: |
.ci/build.sh
- name: Test
run: |
bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
- name: Prepare to upload errors
if: failure()
run: |
dash.exe -c "mkdir -p Tests/errors"
- name: Upload errors
uses: actions/upload-artifact@v4
if: failure()
with:
name: errors
path: Tests/errors
- name: After success
run: |
bash.exe .ci/after_success.sh
rm C:\cygwin\bin\bash.EXE
- name: Upload coverage
uses: codecov/codecov-action@v5
with:
files: ./coverage.xml
flags: GHA_Cygwin
name: Cygwin Python 3.${{ matrix.python-minor-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success:
permissions:
contents: none
needs: build
runs-on: ubuntu-latest
name: Cygwin Test Successful
steps:
- name: Success
run: echo Cygwin Test Successful

View File

@ -4,14 +4,19 @@ on:
push: push:
branches: branches:
- "**" - "**"
paths-ignore: &paths-ignore paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
paths-ignore: *paths-ignore paths-ignore:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -21,50 +26,45 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs: jobs:
build: build:
runs-on: ${{ matrix.os }} runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: ["ubuntu-latest"]
docker: [ docker: [
# Run slower jobs first to give them a headstart and reduce waiting time # Run slower jobs first to give them a headstart and reduce waiting time
ubuntu-26.04-resolute-ppc64le, ubuntu-22.04-jammy-arm64v8,
ubuntu-26.04-resolute-s390x, ubuntu-24.04-noble-ppc64le,
ubuntu-24.04-noble-s390x,
# Then run the remainder # Then run the remainder
alpine, alpine,
amazon-2-amd64,
amazon-2023-amd64, amazon-2023-amd64,
arch, arch,
centos-stream-9-amd64, centos-stream-9-amd64,
centos-stream-10-amd64, debian-12-bookworm-x86,
debian-13-trixie-x86, debian-12-bookworm-amd64,
debian-13-trixie-amd64, fedora-40-amd64,
fedora-43-amd64, fedora-41-amd64,
fedora-44-amd64,
gentoo, gentoo,
ubuntu-22.04-jammy-amd64, ubuntu-22.04-jammy-amd64,
ubuntu-24.04-noble-amd64, ubuntu-24.04-noble-amd64,
ubuntu-26.04-resolute-amd64,
] ]
dockerTag: [main] dockerTag: [main]
include: include:
- docker: "ubuntu-26.04-resolute-ppc64le" - docker: "ubuntu-22.04-jammy-arm64v8"
qemu-arch: "aarch64"
- docker: "ubuntu-24.04-noble-ppc64le"
qemu-arch: "ppc64le" qemu-arch: "ppc64le"
- docker: "ubuntu-26.04-resolute-s390x" - docker: "ubuntu-24.04-noble-s390x"
qemu-arch: "s390x" qemu-arch: "s390x"
- docker: "ubuntu-26.04-resolute-arm64v8"
os: "ubuntu-24.04-arm"
dockerTag: main
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
@ -73,13 +73,12 @@ jobs:
- name: Set up QEMU - name: Set up QEMU
if: "matrix.qemu-arch" if: "matrix.qemu-arch"
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 run: |
with: docker run --rm --privileged aptman/qus -s -- -p ${{ matrix.qemu-arch }}
platforms: ${{ matrix.qemu-arch }}
- name: Docker pull - name: Docker pull
run: | run: |
docker pull ${{ matrix.qemu-arch && format('--platform=linux/{0}', matrix.qemu-arch)}} pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
- name: Docker build - name: Docker build
run: | run: |
@ -90,21 +89,22 @@ jobs:
- name: After success - name: After success
run: | run: |
PATH="$PATH:~/.local/bin"
docker start pillow_container docker start pillow_container
sudo docker cp pillow_container:/Pillow /Pillow
sudo chown -R runner /Pillow
pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'` pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'`
docker stop pillow_container docker stop pillow_container
sudo mkdir -p $pil_path sudo mkdir -p $pil_path
sudo cp src/PIL/*.py $pil_path sudo cp src/PIL/*.py $pil_path
cd /Pillow
.ci/after_success.sh .ci/after_success.sh
env:
MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 uses: codecov/codecov-action@v5
with: with:
flags: GHA_Docker flags: GHA_Docker
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success: success:
permissions: permissions:

View File

@ -4,14 +4,19 @@ on:
push: push:
branches: branches:
- "**" - "**"
paths-ignore: &paths-ignore paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
paths-ignore: *paths-ignore paths-ignore:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -23,7 +28,6 @@ concurrency:
env: env:
COVERAGE_CORE: sysmon COVERAGE_CORE: sysmon
FORCE_COLOR: 1
jobs: jobs:
build: build:
@ -41,7 +45,7 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
@ -56,16 +60,15 @@ jobs:
mingw-w64-x86_64-gcc \ mingw-w64-x86_64-gcc \
mingw-w64-x86_64-ghostscript \ mingw-w64-x86_64-ghostscript \
mingw-w64-x86_64-lcms2 \ mingw-w64-x86_64-lcms2 \
mingw-w64-x86_64-libavif \
mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libjpeg-turbo \ mingw-w64-x86_64-libjpeg-turbo \
mingw-w64-x86_64-libraqm \ mingw-w64-x86_64-libraqm \
mingw-w64-x86_64-libtiff \ mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-python-numpy \ mingw-w64-x86_64-python3-numpy \
mingw-w64-x86_64-python-olefile \ mingw-w64-x86_64-python3-olefile \
mingw-w64-x86_64-python-pip \ mingw-w64-x86_64-python3-pip \
mingw-w64-x86_64-python-pytest \ mingw-w64-x86_64-python-pytest \
mingw-w64-x86_64-python-pytest-cov \ mingw-w64-x86_64-python-pytest-cov \
mingw-w64-x86_64-python-pytest-timeout \ mingw-w64-x86_64-python-pytest-timeout \
@ -82,8 +85,9 @@ jobs:
.ci/test.sh .ci/test.sh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 uses: codecov/codecov-action@v5
with: with:
files: ./coverage.xml files: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows
name: "MSYS2 MinGW" name: "MSYS2 MinGW"
token: ${{ secrets.CODECOV_ORG_TOKEN }}

View File

@ -1,64 +0,0 @@
name: Test Valgrind Memory Leaks
# like the Docker tests, but running valgrind only on *.c/*.h changes.
# this is very expensive. Only run on the pull request.
on:
# push:
# branches:
# - "**"
# paths:
# - ".github/workflows/test-valgrind-memory.yml"
# - "**.c"
# - "**.h"
# - "depends/docker-test-valgrind-memory.sh"
pull_request:
paths:
- ".github/workflows/test-valgrind-memory.yml"
- "**.c"
- "**.h"
- "depends/docker-test-valgrind-memory.sh"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
docker: [
ubuntu-22.04-jammy-amd64-valgrind,
]
dockerTag: [main]
name: ${{ matrix.docker }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Build system information
run: python3 .github/workflows/system-info.py
- name: Docker pull
run: |
docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
- name: Build and Run Valgrind
run: |
# The Pillow user in the docker container is UID 1001
sudo chown -R 1001 $GITHUB_WORKSPACE
docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} /Pillow/depends/docker-test-valgrind-memory.sh
sudo chown -R runner $GITHUB_WORKSPACE

View File

@ -6,12 +6,15 @@ on:
push: push:
branches: branches:
- "**" - "**"
paths: &paths paths:
- ".github/workflows/test-valgrind.yml" - ".github/workflows/test-valgrind.yml"
- "**.c" - "**.c"
- "**.h" - "**.h"
pull_request: pull_request:
paths: *paths paths:
- ".github/workflows/test-valgrind.yml"
- "**.c"
- "**.h"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -21,9 +24,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs: jobs:
build: build:
@ -39,7 +39,7 @@ jobs:
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false

View File

@ -4,14 +4,19 @@ on:
push: push:
branches: branches:
- "**" - "**"
paths-ignore: &paths-ignore paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
paths-ignore: *paths-ignore paths-ignore:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -23,40 +28,34 @@ concurrency:
env: env:
COVERAGE_CORE: sysmon COVERAGE_CORE: sysmon
FORCE_COLOR: 1
jobs: jobs:
build: build:
runs-on: ${{ matrix.os }} runs-on: windows-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14", "3.15"] python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"]
architecture: ["x64"]
os: ["windows-latest"]
include:
# Test the oldest Python on 32-bit
- { python-version: "3.10", architecture: "x86", os: "windows-2022" }
timeout-minutes: 45 timeout-minutes: 30
name: Python ${{ matrix.python-version }} (${{ matrix.architecture }}) name: Python ${{ matrix.python-version }}
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
- name: Checkout cached dependencies - name: Checkout cached dependencies
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/pillow-depends repository: python-pillow/pillow-depends
path: winbuild\depends path: winbuild\depends
- name: Checkout extra test images - name: Checkout extra test images
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/test-images repository: python-pillow/test-images
@ -64,11 +63,10 @@ jobs:
# sets env: pythonLocation # sets env: pythonLocation
- name: Set up Python - name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true
architecture: ${{ matrix.architecture }}
cache: pip cache: pip
cache-dependency-path: ".github/workflows/test-windows.yml" cache-dependency-path: ".github/workflows/test-windows.yml"
@ -80,22 +78,18 @@ jobs:
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
- name: Install CPython dependencies - name: Install CPython dependencies
if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'" if: "!contains(matrix.python-version, 'pypy')"
run: | run: |
python3 -m pip install PyQt6 python3 -m pip install PyQt6
- name: Install PyArrow dependency
run: |
python3 -m pip install --only-binary=:all: pyarrow || true
- name: Install dependencies - name: Install dependencies
id: install id: install
run: | run: |
choco install nasm --no-progress choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.7.0 --no-progress choco install ghostscript --version=10.4.0 --no-progress
echo "C:\Program Files\gs\gs10.07.0\bin" >> $env:GITHUB_PATH echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH
# Install extra test images # Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images xcopy /S /Y Tests\test-images\* Tests\images
@ -108,7 +102,7 @@ jobs:
- name: Cache build - name: Cache build
id: build-cache id: build-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@v4
with: with:
path: winbuild\build path: winbuild\build
key: key:
@ -145,10 +139,6 @@ jobs:
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libpng.cmd" run: "& winbuild\\build\\build_dep_libpng.cmd"
- name: Build dependencies / libavif
if: steps.build-cache.outputs.cache-hit != 'true' && matrix.architecture == 'x64'
run: "& winbuild\\build\\build_dep_libavif.cmd"
# for FreeType WOFF2 font support # for FreeType WOFF2 font support
- name: Build dependencies / brotli - name: Build dependencies / brotli
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
@ -184,9 +174,8 @@ jobs:
# trim ~150MB for each job # trim ~150MB for each job
- name: Optimize build cache - name: Optimize build cache
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: | run: rmdir /S /Q winbuild\build\src
rm -rf winbuild\build\src shell: cmd
shell: bash
- name: Build Pillow - name: Build Pillow
run: | run: |
@ -203,7 +192,9 @@ jobs:
- name: Test Pillow - name: Test Pillow
run: | run: |
path %GITHUB_WORKSPACE%\winbuild\build\bin;%PATH%
.ci\test.cmd .ci\test.cmd
shell: cmd
- name: Prepare to upload errors - name: Prepare to upload errors
if: failure() if: failure()
@ -212,7 +203,7 @@ jobs:
shell: bash shell: bash
- name: Upload errors - name: Upload errors
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: errors name: errors
@ -224,11 +215,12 @@ jobs:
shell: pwsh shell: pwsh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 uses: codecov/codecov-action@v5
with: with:
files: ./coverage.xml files: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows
name: ${{ runner.os }} Python ${{ matrix.python-version }} name: ${{ runner.os }} Python ${{ matrix.python-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success: success:
permissions: permissions:

View File

@ -4,14 +4,19 @@ on:
push: push:
branches: branches:
- "**" - "**"
paths-ignore: &paths-ignore paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
paths-ignore: *paths-ignore paths-ignore:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -24,7 +29,6 @@ concurrency:
env: env:
COVERAGE_CORE: sysmon COVERAGE_CORE: sysmon
FORCE_COLOR: 1 FORCE_COLOR: 1
PIP_DISABLE_PIP_VERSION_CHECK: 1
jobs: jobs:
build: build:
@ -37,34 +41,34 @@ jobs:
"ubuntu-latest", "ubuntu-latest",
] ]
python-version: [ python-version: [
"pypy3.11", "pypy3.10",
"3.15t", "3.13t",
"3.15",
"3.14t",
"3.14",
"3.13", "3.13",
"3.12", "3.12",
"3.11", "3.11",
"3.10", "3.10",
"3.9",
] ]
include: include:
- { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.11", PYTHONOPTIMIZE: 2 } - { python-version: "3.10", PYTHONOPTIMIZE: 2 }
# Intel # Free-threaded
- { os: "macos-26-intel", python-version: "3.10" } - { python-version: "3.13t", disable-gil: true }
# M1 only available for 3.10+
- { os: "macos-13", python-version: "3.9" }
exclude: exclude:
- { os: "macos-latest", python-version: "3.10" } - { os: "macos-latest", python-version: "3.9" }
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: Quansight-Labs/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true
@ -73,42 +77,29 @@ jobs:
".ci/*.sh" ".ci/*.sh"
"pyproject.toml" "pyproject.toml"
- name: Set PYTHON_GIL
if: "${{ matrix.disable-gil }}"
run: |
echo "PYTHON_GIL=0" >> $GITHUB_ENV
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
- 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 - name: Cache libimagequant
if: startsWith(matrix.os, 'ubuntu') if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 uses: actions/cache@v4
id: cache-libimagequant id: cache-libimagequant
with: with:
path: ~/cache-libimagequant path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} 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 - name: Install Linux dependencies
if: startsWith(matrix.os, 'ubuntu') if: startsWith(matrix.os, 'ubuntu')
run: | run: |
.ci/install.sh .ci/install.sh
env: env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }} 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_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
- name: Install macOS dependencies - name: Install macOS dependencies
if: startsWith(matrix.os, 'macOS') if: startsWith(matrix.os, 'macOS')
@ -118,7 +109,7 @@ jobs:
GHA_PYTHON_VERSION: ${{ matrix.python-version }} GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Register gcc problem matcher - name: Register gcc problem matcher
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'" if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'"
run: echo "::add-matcher::.github/problem-matchers/gcc.json" run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Build - name: Build
@ -147,7 +138,7 @@ jobs:
mkdir -p Tests/errors mkdir -p Tests/errors
- name: Upload errors - name: Upload errors
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: errors name: errors
@ -158,10 +149,11 @@ jobs:
.ci/after_success.sh .ci/after_success.sh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 uses: codecov/codecov-action@v5
with: with:
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success: success:
permissions: permissions:

View File

@ -1,151 +1,82 @@
#!/bin/bash #!/bin/bash
# Safety check - Pillow builds require that CIBW_ARCHS is set, and that it only # Setup that needs to be done before multibuild utils are invoked
# contains a single value (even though cibuildwheel allows multiple values in PROJECTDIR=$(pwd)
# CIBW_ARCHS). This check doesn't work on Linux because of how the CIBW_ARCHS if [[ "$(uname -s)" == "Darwin" ]]; then
# variable is exposed. # Safety check - macOS builds require that CIBW_ARCHS is set, and that it
function check_cibw_archs { # only contains a single value (even though cibuildwheel allows multiple
# values in CIBW_ARCHS).
if [[ -z "$CIBW_ARCHS" ]]; then if [[ -z "$CIBW_ARCHS" ]]; then
echo "ERROR: Pillow builds require CIBW_ARCHS be defined." echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined."
exit 1 exit 1
fi fi
if [[ "$CIBW_ARCHS" == *" "* ]]; then if [[ "$CIBW_ARCHS" == *" "* ]]; then
echo "ERROR: Pillow builds only support a single architecture in CIBW_ARCHS." echo "ERROR: Pillow macOS builds only support a single architecture in CIBW_ARCHS."
exit 1 exit 1
fi fi
}
# Setup that needs to be done before multibuild utils are invoked. Process
# potential cross-build platforms before native platforms to ensure that we pick
# up the cross environment.
PROJECTDIR=$(pwd)
if [[ "$CIBW_PLATFORM" == "ios" ]]; then
check_cibw_archs
# On iOS, CIBW_ARCHS is actually a multi-arch - arm64_iphoneos,
# arm64_iphonesimulator or x86_64_iphonesimulator. Split into the CPU
# platform, and the iOS SDK.
PLAT=$(echo $CIBW_ARCHS | sed "s/\(.*\)_\(.*\)/\1/")
IOS_SDK=$(echo $CIBW_ARCHS | sed "s/\(.*\)_\(.*\)/\2/")
# Build iOS builds in `build/iphoneos` or `build/iphonesimulator`
# (depending on the build target). Install them into `build/deps/iphoneos`
# or `build/deps/iphonesimulator`
WORKDIR=$(pwd)/build/$IOS_SDK
BUILD_PREFIX=$(pwd)/build/deps/$IOS_SDK
# GNU tooling insists on using aarch64 rather than arm64
if [[ $PLAT == "arm64" ]]; then
GNU_ARCH=aarch64
else
GNU_ARCH=x86_64
fi
IOS_SDK_PATH=$(xcrun --sdk $IOS_SDK --show-sdk-path)
CMAKE_SYSTEM_NAME=iOS
IOS_HOST_TRIPLE=$PLAT-apple-ios$IPHONEOS_DEPLOYMENT_TARGET
if [[ "$IOS_SDK" == "iphonesimulator" ]]; then
IOS_HOST_TRIPLE=$IOS_HOST_TRIPLE-simulator
fi
# GNU Autotools doesn't recognize the existence of arm64-apple-ios-simulator
# as a valid host. However, the only difference between arm64-apple-ios and
# arm64-apple-ios-simulator is the choice of sysroot, and that is
# coordinated by CC, CFLAGS etc. From the perspective of configure, the two
# platforms are identical, so we can use arm64-apple-ios consistently.
# This (mostly) avoids us needing to patch config.sub in dependency sources.
HOST_CONFIGURE_FLAGS="--disable-shared --enable-static --host=$GNU_ARCH-apple-ios --build=$GNU_ARCH-apple-darwin"
# CMake has native support for iOS. However, most of that support is based
# on using the Xcode builder, which isn't very helpful for most of Pillow's
# dependencies. Therefore, we lean on the OSX configurations, plus CC, CFLAGS
# etc. to ensure the right sysroot is selected.
HOST_CMAKE_FLAGS="-DCMAKE_SYSTEM_NAME=$CMAKE_SYSTEM_NAME -DCMAKE_SYSTEM_PROCESSOR=$GNU_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET -DCMAKE_OSX_SYSROOT=$IOS_SDK_PATH -DBUILD_SHARED_LIBS=NO -DENABLE_SHARED=NO"
# Meson needs to be pointed at a cross-platform configuration file
# This will be generated once CC etc. have been evaluated.
HOST_MESON_FLAGS="--cross-file $WORKDIR/meson-cross.txt -Dprefer_static=true -Ddefault_library=static"
elif [[ "$(uname -s)" == "Darwin" ]]; then
check_cibw_archs
# Build macOS dependencies in `build/darwin` # Build macOS dependencies in `build/darwin`
# Install them into `build/deps/darwin` # Install them into `build/deps/darwin`
PLAT=$CIBW_ARCHS
WORKDIR=$(pwd)/build/darwin WORKDIR=$(pwd)/build/darwin
BUILD_PREFIX=$(pwd)/build/deps/darwin BUILD_PREFIX=$(pwd)/build/deps/darwin
else else
# Build prefix will default to /usr/local # Build prefix will default to /usr/local
PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}"
WORKDIR=$(pwd)/build WORKDIR=$(pwd)/build
MB_ML_LIBC=${AUDITWHEEL_POLICY::9} MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
MB_ML_VER=${AUDITWHEEL_POLICY:9} MB_ML_VER=${AUDITWHEEL_POLICY:9}
fi fi
PLAT=$CIBW_ARCHS
# Define custom utilities # Define custom utilities
source wheels/multibuild/common_utils.sh source wheels/multibuild/common_utils.sh
source wheels/multibuild/library_builders.sh source wheels/multibuild/library_builders.sh
if [[ -z "$IS_MACOS" ]]; then if [ -z "$IS_MACOS" ]; then
source wheels/multibuild/manylinux_utils.sh source wheels/multibuild/manylinux_utils.sh
fi fi
ARCHIVE_SDIR=pillow-depends-main ARCHIVE_SDIR=pillow-depends-main
VERSIONS_FILE="$PROJECTDIR/.github/dependencies.json" # Package versions for fresh source builds
_get_ver() { python3 -c "import json; print(json.load(open('$VERSIONS_FILE'))['$1'])"; } FREETYPE_VERSION=2.13.2
FREETYPE_VERSION=$(_get_ver freetype) HARFBUZZ_VERSION=10.1.0
HARFBUZZ_VERSION=$(_get_ver harfbuzz) LIBPNG_VERSION=1.6.44
LIBPNG_VERSION=$(_get_ver libpng) JPEGTURBO_VERSION=3.1.0
JPEGTURBO_VERSION=$(_get_ver jpegturbo) OPENJPEG_VERSION=2.5.3
OPENJPEG_VERSION=$(_get_ver openjpeg) XZ_VERSION=5.6.3
XZ_VERSION=$(_get_ver xz) TIFF_VERSION=4.6.0
ZSTD_VERSION=$(_get_ver zstd) LCMS2_VERSION=2.16
TIFF_VERSION=$(_get_ver tiff) if [[ -n "$IS_MACOS" ]]; then
LCMS2_VERSION=$(_get_ver lcms2) GIFLIB_VERSION=5.2.2
ZLIB_NG_VERSION=$(_get_ver zlib-ng) else
LIBWEBP_VERSION=$(_get_ver libwebp) GIFLIB_VERSION=5.2.1
BZIP2_VERSION=$(_get_ver bzip2) fi
LIBXCB_VERSION=$(_get_ver libxcb) ZLIB_NG_VERSION=2.2.2
BROTLI_VERSION=$(_get_ver brotli) LIBWEBP_VERSION=1.5.0
LIBAVIF_VERSION=$(_get_ver libavif) BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0
function build_pkg_config { function build_pkg_config {
if [ -e pkg-config-stamp ]; then return; fi if [ -e pkg-config-stamp ]; then return; fi
# This essentially duplicates the Homebrew recipe. # This essentially duplicates the Homebrew recipe
# On iOS, we need a binary that can be executed on the build machine; but we ORIGINAL_CFLAGS=$CFLAGS
# can create a host-specific pc-path to store iOS .pc files. To ensure a CFLAGS="$CFLAGS -Wno-int-conversion"
# macOS-compatible build, we temporarily clear environment flags that set build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \
# iOS-specific values.
if [[ -n "$IOS_SDK" ]]; then
ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS
ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET
unset HOST_CONFIGURE_FLAGS
unset IPHONEOS_DEPLOYMENT_TARGET
fi
CFLAGS="$CFLAGS -Wno-int-conversion" CPPFLAGS="" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \
--disable-debug --disable-host-tool --with-internal-glib \ --disable-debug --disable-host-tool --with-internal-glib \
--with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \ --with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \
--with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include --with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include
CFLAGS=$ORIGINAL_CFLAGS
if [[ -n "$IOS_SDK" ]]; then
HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS
IPHONEOS_DEPLOYMENT_TARGET=$ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET
fi;
export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config
touch pkg-config-stamp touch pkg-config-stamp
} }
function build_zlib_ng { function build_zlib_ng {
if [ -e zlib-stamp ]; then return; fi if [ -e zlib-stamp ]; then return; fi
# zlib-ng uses a "configure" script, but it's not a GNU autotools script, so fetch_unpack https://github.com/zlib-ng/zlib-ng/archive/$ZLIB_NG_VERSION.tar.gz zlib-ng-$ZLIB_NG_VERSION.tar.gz
# it doesn't honor the usual flags. Temporarily disable any (cd zlib-ng-$ZLIB_NG_VERSION \
# cross-compilation flags. && ./configure --prefix=$BUILD_PREFIX --zlib-compat \
ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS && make -j4 \
unset HOST_CONFIGURE_FLAGS && make install)
build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat
HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS
touch zlib-stamp touch zlib-stamp
} }
@ -153,8 +84,8 @@ function build_brotli {
if [ -e brotli-stamp ]; then return; fi 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) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
(cd $out_dir \ (cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib -DCMAKE_MACOSX_BUNDLE=OFF $HOST_CMAKE_FLAGS . \ && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make -j4 install) && make install)
touch brotli-stamp touch brotli-stamp
} }
@ -164,181 +95,64 @@ function build_harfbuzz {
local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz)
(cd $out_dir \ (cd $out_dir \
&& meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled $HOST_MESON_FLAGS) && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled)
(cd $out_dir/build \ (cd $out_dir/build \
&& meson install) && meson install)
touch harfbuzz-stamp touch harfbuzz-stamp
} }
function build_libavif {
if [ -e libavif-stamp ]; then return; fi
python3 -m pip install meson ninja
if ([[ "$PLAT" == "x86_64" ]] && [[ -z "$IOS_SDK" ]]) || [ -n "$SANITIZER" ]; then
build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03
fi
local build_shared=ON
local lto=ON
local libavif_cmake_flags
if [[ -n "$IS_MACOS" ]]; then
lto=OFF
libavif_cmake_flags=(
-DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
-DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \
)
if [[ -n "$IOS_SDK" ]]; then
build_shared=OFF
fi
else
libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now")
fi
if [[ -n "$IOS_SDK" ]] && [[ "$PLAT" == "x86_64" ]]; then
libavif_cmake_flags+=(-DAOM_TARGET_CPU=generic)
else
libavif_cmake_flags+=(
-DAVIF_CODEC_AOM_DECODE=OFF \
-DAVIF_CODEC_DAV1D=LOCAL
)
fi
local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz)
# CONFIG_AV1_HIGHBITDEPTH=0 is a flag for libaom (included as a subproject
# of libavif) that disables support for encoding high bit depth images.
(cd $out_dir \
&& cmake \
-DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \
-DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \
-DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \
-DBUILD_SHARED_LIBS=$build_shared \
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
-DAVIF_CODEC_AOM=LOCAL \
-DCONFIG_AV1_HIGHBITDEPTH=0 \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \
-DCMAKE_C_VISIBILITY_PRESET=hidden \
-DCMAKE_CXX_VISIBILITY_PRESET=hidden \
-DCMAKE_BUILD_TYPE=MinSizeRel \
"${libavif_cmake_flags[@]}" \
$HOST_CMAKE_FLAGS . )
if [[ -n "$IOS_SDK" ]]; then
# libavif's CMake configuration generates a meson cross file... but it
# doesn't work for iOS cross-compilation. Copy in Pillow-generated
# meson-cross config to replace the cmake-generated version.
cp $WORKDIR/meson-cross.txt $out_dir/crossfile-apple.meson
fi
(cd $out_dir && make -j4 install)
touch libavif-stamp
}
function build_zstd {
if [ -e zstd-stamp ]; then return; fi
local out_dir=$(fetch_unpack https://github.com/facebook/zstd/releases/download/v$ZSTD_VERSION/zstd-$ZSTD_VERSION.tar.gz)
(cd $out_dir \
&& make -j4 install)
touch zstd-stamp
}
function build { function build {
build_xz build_xz
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
yum remove -y zlib-devel yum remove -y zlib-devel
fi fi
if [[ -n "$IS_MACOS" ]]; then build_zlib_ng
CFLAGS="$CFLAGS -headerpad_max_install_names" build_zlib_ng
else
build_zlib_ng
fi
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [[ -n "$IS_MACOS" ]]; then if [ -n "$IS_MACOS" ]; then
build_simple xorgproto 2025.1 https://www.x.org/pub/individual/proto build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
else else
sed "s/\${pc_sysrootdir\}//" $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc
fi fi
build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib
build_libjpeg_turbo build_libjpeg_turbo
if [[ -n "$IS_MACOS" ]]; then if [ -n "$IS_MACOS" ]; then
# Custom tiff build to include jpeg; by default, configure won't include # Custom tiff build to include jpeg; by default, configure won't include
# headers/libs in the custom macOS/iOS prefix. Explicitly disable webp, # headers/libs in the custom macOS prefix. Explicitly disable webp,
# libdeflate and zstd, because on x86_64 macs, it will pick up the # libdeflate and zstd, because on x86_64 macs, it will pick up the
# Homebrew versions of those libraries from /usr/local. # Homebrew versions of those libraries from /usr/local.
build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \
--with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \
--disable-webp --disable-libdeflate --disable-zstd --disable-webp --disable-libdeflate --disable-zstd
else else
build_zstd
build_tiff build_tiff
fi fi
build_libavif
build_libpng build_libpng
build_lcms2 build_lcms2
build_openjpeg build_openjpeg
webp_cflags="-O3 -DNDEBUG" ORIGINAL_CFLAGS=$CFLAGS
CFLAGS="$CFLAGS -O3 -DNDEBUG"
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then
webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names" CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
fi fi
webp_ldflags="" build_libwebp
if [[ -n "$IOS_SDK" ]]; then CFLAGS=$ORIGINAL_CFLAGS
webp_ldflags="$webp_ldflags -llzma -lz"
fi
CFLAGS="$CFLAGS $webp_cflags" LDFLAGS="$LDFLAGS $webp_ldflags" build_simple libwebp $LIBWEBP_VERSION \
https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
--enable-libwebpmux --enable-libwebpdemux
build_brotli build_brotli
if [[ -n "$IS_MACOS" ]]; then if [ -n "$IS_MACOS" ]; then
# Custom freetype build # Custom freetype build
build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
else else
build_freetype build_freetype
fi fi
if [[ -z "$IOS_SDK" ]]; then build_harfbuzz
# On iOS, there's no vendor-provided raqm, and we can't ship it due to
# licensing, so there's no point building harfbuzz.
build_harfbuzz
fi
}
function create_meson_cross_config {
cat << EOF > $WORKDIR/meson-cross.txt
[binaries]
pkg-config = '$BUILD_PREFIX/bin/pkg-config'
cmake = '$(which cmake)'
c = '$CC'
cpp = '$CXX'
strip = '$STRIP'
[built-in options]
c_args = '$CFLAGS -I$BUILD_PREFIX/include'
cpp_args = '$CXXFLAGS -I$BUILD_PREFIX/include'
c_link_args = '$CFLAGS -L$BUILD_PREFIX/lib'
cpp_link_args = '$CFLAGS -L$BUILD_PREFIX/lib'
[host_machine]
system = 'darwin'
subsystem = 'ios'
kernel = 'xnu'
cpu_family = '$(uname -m)'
cpu = '$(uname -m)'
endian = 'little'
EOF
} }
# Perform all dependency builds in the build subfolder. # Perform all dependency builds in the build subfolder.
@ -357,53 +171,28 @@ if [[ ! -d $WORKDIR/pillow-depends-main ]]; then
fi fi
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then
# Homebrew (or similar packaging environments) install can contain some of
# the libraries that we're going to build. However, they may be compiled
# with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use,
# and they may bring in other dependencies that we don't want. The same will
# be true of any other locations on the path. To avoid conflicts, strip the
# path down to the bare minimum (which, on macOS, won't include any
# development dependencies).
export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
export CMAKE_PREFIX_PATH=$BUILD_PREFIX
# Ensure the basic structure of the build prefix directory exists. # Ensure the basic structure of the build prefix directory exists.
mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/bin"
mkdir -p "$BUILD_PREFIX/lib" mkdir -p "$BUILD_PREFIX/lib"
# Ensure pkg-config is available. This is done *before* setting CC, CFLAGS # Ensure pkg-config is available
# etc. to ensure that the build is *always* a macOS build, even when building
# for iOS.
build_pkg_config build_pkg_config
# Ensure cmake is available
# Ensure cmake is available, and that the default prefix used by CMake is
# the build prefix
python3 -m pip install cmake python3 -m pip install cmake
export CMAKE_PREFIX_PATH=$BUILD_PREFIX
if [[ -n "$IOS_SDK" ]]; then
export AR="$(xcrun --find --sdk $IOS_SDK ar)"
export CPP="$(xcrun --find --sdk $IOS_SDK clang) -E"
export CC=$(xcrun --find --sdk $IOS_SDK clang)
export CXX=$(xcrun --find --sdk $IOS_SDK clang++)
export LD=$(xcrun --find --sdk $IOS_SDK ld)
export STRIP=$(xcrun --find --sdk $IOS_SDK strip)
CPPFLAGS="$CPPFLAGS --sysroot=$IOS_SDK_PATH"
CFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET"
CXXFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET"
# Having IPHONEOS_DEPLOYMENT_TARGET in the environment causes problems
# with some cross-building toolchains, because it introduces implicit
# behavior into clang.
unset IPHONEOS_DEPLOYMENT_TARGET
# Now that we know CC etc., we can create a meson cross-configuration file
create_meson_cross_config
fi
fi fi
wrap_wheel_builder build wrap_wheel_builder build
# A safety catch for iOS. iOS can't use dynamic libraries, but clang will prefer
# to link dynamic libraries to static libraries. The only way to reliably
# prevent this is to not have dynamic libraries available in the first place.
# The build process *shouldn't* generate any dylibs... but just in case, purge
# any dylibs that *have* been installed into the build prefix directory.
if [[ -n "$IOS_SDK" ]]; then
find "$BUILD_PREFIX" -name "*.dylib" -exec rm -rf {} \;
fi
# Return to the project root to finish the build # Return to the project root to finish the build
popd > /dev/null popd > /dev/null

View File

@ -9,18 +9,14 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null
} }
$env:path += ";$pillow\winbuild\build\bin\" $env:path += ";$pillow\winbuild\build\bin\"
if (Test-Path $venv\Scripts\pypy.exe) { & "$venv\Scripts\activate.ps1"
$python = "pypy.exe"
} else {
$python = "python.exe"
}
& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
cd $pillow cd $pillow
& $venv\Scripts\$python -VV & python -VV
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }
& $venv\Scripts\$python selftest.py & python selftest.py
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }
& $venv\Scripts\$python -m pytest -vv -x checks\check_wheel.py & python -m pytest -vx Tests\check_wheel.py
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }
& $venv\Scripts\$python -m pytest -vv -x Tests & python -m pytest -vx Tests
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }

View File

@ -25,6 +25,8 @@ else
yum install -y fribidi yum install -y fribidi
fi fi
python3 -m pip install numpy
if [ ! -d "test-images-main" ]; then if [ ! -d "test-images-main" ]; then
curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
unzip pillow-test-images.zip unzip pillow-test-images.zip
@ -33,5 +35,5 @@ fi
# Runs tests # Runs tests
python3 selftest.py python3 selftest.py
python3 -m pytest -vv -x checks/check_wheel.py python3 -m pytest Tests/check_wheel.py
python3 -m pytest -vv -x python3 -m pytest

View File

@ -10,14 +10,9 @@ on:
# │ │ │ │ │ # │ │ │ │ │
- cron: "42 1 * * 0,3" - cron: "42 1 * * 0,3"
push: push:
paths: &paths paths:
- ".ci/requirements-cibw.txt" - ".ci/requirements-cibw.txt"
- ".ci/requirements-sbom.txt" - ".github/workflows/wheel*"
- ".github/compare-dist-sizes.py"
- ".github/dependencies.json"
- ".github/generate-sbom.py"
- ".github/workflows/wheels*"
- "pyproject.toml"
- "setup.py" - "setup.py"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py" - "winbuild/build_prepare.py"
@ -25,7 +20,13 @@ on:
tags: tags:
- "*" - "*"
pull_request: pull_request:
paths: *paths paths:
- ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*"
- "setup.py"
- "wheels/*"
- "winbuild/build_prepare.py"
- "winbuild/fribidi.cmake"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -36,12 +37,66 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
EXPECTED_DISTS: 66
FORCE_COLOR: 1 FORCE_COLOR: 1
jobs: jobs:
build-native-wheels: build-1-QEMU-emulated-wheels:
if: github.event_name != 'schedule' || github.event.repository.fork == false if: github.event_name != 'schedule'
name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- pp310
- cp3{9,10,11}
- cp3{12,13}
spec:
- manylinux2014
- manylinux_2_28
- musllinux
exclude:
- { python-version: pp310, spec: musllinux }
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: true
- uses: actions/setup-python@v5
with:
python-version: "3.x"
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Install cibuildwheel
run: |
python3 -m pip install -r .ci/requirements-cibw.txt
- name: Build wheels
run: |
python3 -m cibuildwheel --output-dir wheelhouse
env:
# Build only the currently selected Linux architecture (so we can
# parallelise for speed).
CIBW_ARCHS: "aarch64"
# Likewise, select only one Python version per job to speed this up.
CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
CIBW_ENABLE: cpython-prerelease
# Extra options for manylinux.
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
- uses: actions/upload-artifact@v4
with:
name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }}
path: ./wheelhouse/*.whl
build-2-native-wheels:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: ${{ matrix.name }} name: ${{ matrix.name }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@ -49,67 +104,39 @@ jobs:
matrix: matrix:
include: include:
- name: "macOS 10.10 x86_64" - name: "macOS 10.10 x86_64"
platform: macos os: macos-13
os: macos-26-intel
cibw_arch: x86_64 cibw_arch: x86_64
build: "cp3{10,11}*" build: "cp3{9,10,11}*"
macosx_deployment_target: "10.10" macosx_deployment_target: "10.10"
- name: "macOS 10.13 x86_64" - name: "macOS 10.13 x86_64"
platform: macos os: macos-13
os: macos-26-intel
cibw_arch: x86_64 cibw_arch: x86_64
build: "cp3{12,13}*" build: "cp3{12,13}*"
macosx_deployment_target: "10.13" macosx_deployment_target: "10.13"
- name: "macOS 10.15 x86_64" - name: "macOS 10.15 x86_64"
platform: macos os: macos-13
os: macos-26-intel
cibw_arch: x86_64 cibw_arch: x86_64
build: "{cp314,pp3}*" build: "pp310*"
macosx_deployment_target: "10.15" macosx_deployment_target: "10.15"
- name: "macOS arm64" - name: "macOS arm64"
platform: macos
os: macos-latest os: macos-latest
cibw_arch: arm64 cibw_arch: arm64
macosx_deployment_target: "11.0" macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64"
os: ubuntu-latest
cibw_arch: x86_64
- name: "manylinux_2_28 x86_64" - name: "manylinux_2_28 x86_64"
platform: linux
os: ubuntu-latest os: ubuntu-latest
cibw_arch: x86_64 cibw_arch: x86_64
build: "*manylinux*" build: "*manylinux*"
- name: "musllinux x86_64" manylinux: "manylinux_2_28"
platform: linux
os: ubuntu-latest
cibw_arch: x86_64
build: "*musllinux*"
- name: "manylinux_2_28 aarch64"
platform: linux
os: ubuntu-24.04-arm
cibw_arch: aarch64
build: "*manylinux*"
- name: "musllinux aarch64"
platform: linux
os: ubuntu-24.04-arm
cibw_arch: aarch64
build: "*musllinux*"
- name: "iOS arm64 device"
platform: ios
os: macos-latest
cibw_arch: arm64_iphoneos
- name: "iOS arm64 simulator"
platform: ios
os: macos-latest
cibw_arch: arm64_iphonesimulator
- name: "iOS x86_64 simulator"
platform: ios
os: macos-26-intel
cibw_arch: x86_64_iphonesimulator
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
submodules: true submodules: true
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
@ -121,44 +148,43 @@ jobs:
run: | run: |
python3 -m cibuildwheel --output-dir wheelhouse python3 -m cibuildwheel --output-dir wheelhouse
env: env:
CIBW_PLATFORM: ${{ matrix.platform }}
CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }} CIBW_BUILD: ${{ matrix.build }}
CIBW_ENABLE: cpython-prerelease pypy CIBW_ENABLE: cpython-prerelease cpython-freethreading
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_SKIP: pp39-*
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - uses: actions/upload-artifact@v4
with: with:
name: dist-${{ matrix.name }} name: dist-${{ matrix.os }}${{ matrix.macosx_deployment_target && format('-{0}', matrix.macosx_deployment_target) }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
windows: windows:
if: github.event_name != 'schedule' || github.event.repository.fork == false if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: Windows ${{ matrix.cibw_arch }} name: Windows ${{ matrix.cibw_arch }}
runs-on: ${{ matrix.os }} runs-on: windows-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- cibw_arch: x86 - cibw_arch: x86
os: windows-latest
- cibw_arch: AMD64 - cibw_arch: AMD64
os: windows-latest
- cibw_arch: ARM64 - cibw_arch: ARM64
os: windows-11-arm
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
- name: Checkout extra test images - name: Checkout extra test images
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
repository: python-pillow/test-images repository: python-pillow/test-images
path: Tests\test-images path: Tests\test-images
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
@ -179,23 +205,30 @@ jobs:
- name: Build wheels - name: Build wheels
run: | run: |
for f in winbuild/build/license/*; do setlocal EnableDelayedExpansion
name=$(basename "${f%.*}") for %%f in (winbuild\build\license\*) do (
# Skip FriBiDi license, it is not included in the wheel. set x=%%~nf
[[ $name == fribidi* ]] && continue rem Skip FriBiDi license, it is not included in the wheel.
# Skip imagequant license, it is not included in the wheel. set fribidi=!x:~0,7!
[[ $name == libimagequant* ]] && continue if NOT !fribidi!==fribidi (
echo "" >> LICENSE rem Skip imagequant license, it is not included in the wheel.
echo "===== $name =====" >> LICENSE set libimagequant=!x:~0,13!
echo "" >> LICENSE if NOT !libimagequant!==libimagequant (
cat "$f" >> LICENSE echo. >> LICENSE
done echo ===== %%~nf ===== >> LICENSE
cmd //c "winbuild\\build\\build_env.cmd && $pythonLocation\\python.exe -m cibuildwheel . --output-dir wheelhouse" echo. >> LICENSE
type %%f >> LICENSE
)
)
)
call winbuild\\build\\build_env.cmd
%pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse
env: env:
CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw" CIBW_CACHE_PATH: "C:\\cibw"
CIBW_ENABLE: cpython-prerelease pypy CIBW_ENABLE: cpython-prerelease cpython-freethreading
CIBW_SKIP: pp39-*
CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow -v {project}:C:\pillow
@ -204,151 +237,62 @@ jobs:
-e CI -e GITHUB_ACTIONS -e CI -e GITHUB_ACTIONS
mcr.microsoft.com/windows/servercore:ltsc2022 mcr.microsoft.com/windows/servercore:ltsc2022
powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test' powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test'
shell: bash shell: cmd
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@v4
with: with:
name: dist-windows-${{ matrix.cibw_arch }} name: dist-windows-${{ matrix.cibw_arch }}
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
- name: Upload fribidi.dll - name: Upload fribidi.dll
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@v4
with: with:
name: fribidi-windows-${{ matrix.cibw_arch }} name: fribidi-windows-${{ matrix.cibw_arch }}
path: winbuild\build\bin\fribidi* path: winbuild\build\bin\fribidi*
sdist: sdist:
if: github.event_name != 'schedule' || github.event.repository.fork == false if: github.event_name != 'schedule'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
cache: pip
cache-dependency-path: "Makefile"
- run: make sdist - run: make sdist
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - uses: actions/upload-artifact@v4
with: with:
name: dist-sdist name: dist-sdist
path: dist/*.tar.gz path: dist/*.tar.gz
count-dists:
needs: [build-native-wheels, windows, sdist]
runs-on: ubuntu-latest
name: Count dists
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: dist-*
path: dist
merge-multiple: true
- name: "What did we get?"
run: |
ls -alR
echo "Number of dists, should be $EXPECTED_DISTS:"
files=$(ls dist 2>/dev/null | wc -l)
echo $files
[ "$files" -eq $EXPECTED_DISTS ] || exit 1
compare-dist-sizes:
needs: [build-native-wheels, windows, sdist]
runs-on: ubuntu-latest
name: Compare dist sizes vs PyPI
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: dist-*
path: dist
merge-multiple: true
- name: Compare dist sizes vs latest PyPI release
run: uv run .github/compare-dist-sizes.py dist
scientific-python-nightly-wheels-publish: scientific-python-nightly-wheels-publish:
if: github.event.repository.fork == false && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
needs: count-dists needs: [build-2-native-wheels, windows]
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels 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: steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - uses: actions/download-artifact@v4
with: with:
pattern: dist-!(sdist)* pattern: dist-*
path: dist path: dist
merge-multiple: true merge-multiple: true
- name: Upload wheels to scientific-python-nightly-wheels - name: Upload wheels to scientific-python-nightly-wheels
uses: scientific-python/upload-nightly-action@e76cfec8a4611fd02808a801b0ff5a7d7c1b2d99 # 0.6.4 uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1
with: with:
artifacts_path: dist artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
sbom:
if: github.event_name != 'schedule' || github.event.repository.fork == false
runs-on: ubuntu-latest
name: Generate SBOM
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.x"
- name: Generate CycloneDX SBOM
run: python3 .github/generate-sbom.py
- name: Upload SBOM as workflow artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sbom
path: "pillow-*.cdx.json"
- name: Validate SBOM
run: |
python3 -m pip install -r .ci/requirements-sbom.txt
check-jsonschema --schemafile "https://raw.githubusercontent.com/CycloneDX/specification/1.7/schema/bom-1.7.schema.json" pillow-*.cdx.json
sbom-publish:
if: |
github.event.repository.fork == false
&& github.event_name == 'push'
&& startsWith(github.ref, 'refs/tags')
needs: [count-dists, sbom]
runs-on: ubuntu-latest
name: Publish SBOM to GitHub release
permissions:
contents: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: sbom
path: .
- name: Attach SBOM to GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "$GITHUB_REF_NAME" pillow-*.cdx.json
pypi-publish: pypi-publish:
if: github.event.repository.fork == false && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
needs: count-dists needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Upload release to PyPI name: Upload release to PyPI
environment: environment:
@ -357,12 +301,12 @@ jobs:
permissions: permissions:
id-token: write id-token: write
steps: steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - uses: actions/download-artifact@v4
with: with:
pattern: dist-* pattern: dist-*
path: dist path: dist
merge-multiple: true merge-multiple: true
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 uses: pypa/gh-action-pypi-publish@release/v1
with: with:
attestations: true attestations: true

3
.gitignore vendored
View File

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

View File

@ -1,30 +1,30 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.12 rev: v0.8.1
hooks: hooks:
- id: ruff-check - id: ruff
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 26.3.1 rev: 24.10.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.9.4 rev: 1.8.0
hooks: hooks:
- id: bandit - id: bandit
args: [--severity-level=high] args: [--severity-level=high]
files: ^src/ files: ^src/
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.6 rev: v1.5.5
hooks: hooks:
- id: remove-tabs - id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v22.1.4 rev: v19.1.4
hooks: hooks:
- id: clang-format - id: clang-format
types: [c] types: [c]
@ -36,53 +36,44 @@ repos:
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 rev: v5.0.0
hooks: hooks:
- id: check-case-conflict
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable - id: check-shebang-scripts-are-executable
- id: check-merge-conflict - id: check-merge-conflict
- id: check-json - id: check-json
- id: check-toml - id: check-toml
- id: check-yaml - id: check-yaml
args: [--allow-multiple-documents]
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: ^Tests/images/ exclude: ^Tests/images/
- id: file-contents-sorter
files: .github/workflows/Brewfile
- id: trailing-whitespace - id: trailing-whitespace
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.37.2 rev: 0.30.0
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
- id: check-readthedocs - id: check-readthedocs
- id: check-renovate - id: check-renovate
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.24.1
hooks:
- id: zizmor
- repo: https://github.com/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v1.0.2 rev: v1.0.0
hooks: hooks:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.21.1 rev: v2.5.0
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.25 rev: v0.23
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject
additional_dependencies: [trove-classifiers>=2024.10.12] additional_dependencies: [trove-classifiers>=2024.10.12]
- repo: https://github.com/tox-dev/tox-ini-fmt - repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.7.1 rev: 1.4.1
hooks: hooks:
- id: tox-ini-fmt - id: tox-ini-fmt

View File

@ -1,8 +1,5 @@
version: 2 version: 2
sphinx:
configuration: docs/conf.py
formats: [pdf] formats: [pdf]
build: build:

View File

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

View File

@ -13,8 +13,6 @@ include LICENSE
include Makefile include Makefile
include tox.ini include tox.ini
graft Tests graft Tests
graft Tests/images
graft checks
graft src graft src
graft depends graft depends
graft winbuild graft winbuild
@ -22,25 +20,15 @@ graft docs
graft _custom_build graft _custom_build
# build/src control detritus # build/src control detritus
exclude .appveyor.yml
exclude .clang-format exclude .clang-format
exclude .coveragerc exclude .coveragerc
exclude .editorconfig exclude .editorconfig
exclude .readthedocs.yml exclude .readthedocs.yml
exclude codecov.yml exclude codecov.yml
exclude renovate.json exclude renovate.json
exclude Tests/images/README.md
exclude Tests/images/crash*.tif
exclude Tests/images/string_dimension.tiff
global-exclude .git* global-exclude .git*
global-exclude *.pyc global-exclude *.pyc
global-exclude *.so global-exclude *.so
prune .ci prune .ci
prune wheels prune wheels
prune winbuild/build
prune winbuild/depends
prune Tests/errors
prune Tests/images/jpeg2000
prune Tests/images/msp
prune Tests/images/picins
prune Tests/images/sunraster
prune Tests/test-images

View File

@ -23,10 +23,6 @@ doc html:
htmlview: htmlview:
$(MAKE) -C docs htmlview $(MAKE) -C docs htmlview
.PHONY: htmllive
htmllive:
$(MAKE) -C docs htmllive
.PHONY: doccheck .PHONY: doccheck
doccheck: doccheck:
$(MAKE) doc $(MAKE) doc
@ -47,7 +43,6 @@ help:
@echo " docserve run an HTTP server on the docs directory" @echo " docserve run an HTTP server on the docs directory"
@echo " html make HTML docs" @echo " html make HTML docs"
@echo " htmlview open the index page built by the html target in your browser" @echo " htmlview open the index page built by the html target in your browser"
@echo " htmllive rebuild and reload HTML files in your browser"
@echo " install make and install" @echo " install make and install"
@echo " install-coverage make and install with C coverage" @echo " install-coverage make and install with C coverage"
@echo " lint run the lint checks" @echo " lint run the lint checks"
@ -75,7 +70,7 @@ debug:
.PHONY: release-test .PHONY: release-test
release-test: release-test:
python3 checks/check_release_notes.py python3 Tests/check_release_notes.py
python3 -m pip install -e .[tests] python3 -m pip install -e .[tests]
python3 selftest.py python3 selftest.py
python3 -m pytest Tests python3 -m pytest Tests
@ -97,27 +92,13 @@ test:
python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
python3 -m pytest -qq python3 -m pytest -qq
.PHONY: test-p
test-p:
python3 -c "import xdist" > /dev/null 2>&1 || python3 -m pip install pytest-xdist
python3 -m pytest -qq -n auto
.PHONY: valgrind .PHONY: valgrind
valgrind: valgrind:
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
--log-file=/tmp/valgrind-output \ --log-file=/tmp/valgrind-output \
python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output
.PHONY: valgrind-leak
valgrind-leak:
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \
--leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \
--log-file=/tmp/valgrind-output \
python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output
.PHONY: readme .PHONY: readme
readme: readme:
python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2

View File

@ -6,13 +6,11 @@
## Python Imaging Library (Fork) ## Python Imaging Library (Fork)
Pillow is the friendly PIL fork by [Jeffrey 'Alex' Clark and Pillow is the friendly PIL fork by [Jeffrey A. Clark and
contributors](https://github.com/python-pillow/Pillow/graphs/contributors). contributors](https://github.com/python-pillow/Pillow/graphs/contributors).
PIL is the Python Imaging Library by Fredrik Lundh and contributors. PIL is the Python Imaging Library by Fredrik Lundh and contributors.
Development is supported by: As of 2019, Pillow development is
- [Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise) (since 2018) [supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise).
- [Thanks.dev](https://thanks.dev) (since 2023)
- [GitHub Sponsors](https://github.com/sponsors/python-pillow) (since 2026)
<table> <table>
<tr> <tr>
@ -38,9 +36,15 @@ Development is supported by:
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
alt="GitHub Actions build status (Test MinGW)" alt="GitHub Actions build status (Test MinGW)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml"><img
alt="GitHub Actions build status (Test Cygwin)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
alt="GitHub Actions build status (Test Docker)" alt="GitHub Actions build status (Test Docker)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
alt="AppVeyor CI build status (Windows)"
src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img <a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
alt="GitHub Actions build status (Wheels)" alt="GitHub Actions build status (Wheels)"
src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
@ -94,7 +98,7 @@ This library provides extensive file format support, an efficient internal repre
The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool. The core image library is designed for fast access to data stored in a few basic pixel formats. It should provide a solid foundation for a general image processing tool.
## More information ## More Information
- [Documentation](https://pillow.readthedocs.io/) - [Documentation](https://pillow.readthedocs.io/)
- [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html) - [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html)
@ -106,10 +110,6 @@ The core image library is designed for fast access to data stored in a few basic
- [Changelog](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/releases)
- [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork)
## Report a vulnerability ## Report a Vulnerability
To report sensitive vulnerability information, report it [privately on GitHub](https://github.com/python-pillow/Pillow/security/advisories/new). To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security).
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

@ -1,15 +1,34 @@
# Release checklist # Release Checklist
See https://pillow.readthedocs.io/en/stable/releasenotes/versioning.html for See https://pillow.readthedocs.io/en/stable/releasenotes/versioning.html for
information about how the version numbers line up with releases. information about how the version numbers line up with releases.
## Main release ## Main Release
Released quarterly on January 2nd, April 1st, July 1st and October 15th. Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Create a new issue and select the "Maintainers only: Release" template. * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `main` branch.
## Point release * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
* [ ] Create branch and tag for release e.g.:
```bash
git branch 5.2.x
git tag 5.2.0
git push --tags
```
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
has passed, including the "Upload release to PyPI" job. This will have been triggered
by the new tag.
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases).
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/),
increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
```bash
git push --all
```
## Point Release
Released as needed for security, installation or critical bug fixes. Released as needed for security, installation or critical bug fixes.
@ -19,8 +38,7 @@ Released as needed for security, installation or critical bug fixes.
git checkout -t remotes/origin/5.2.x git checkout -t remotes/origin/5.2.x
``` ```
* [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`. * [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`.
* [ ] If this is a security fix: amend commits to include the CVE identifier in the commit message. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`.
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in release branch e.g. `5.2.x`.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Run pre-release check via `make release-test`. * [ ] Run pre-release check via `make release-test`.
* [ ] Create tag for release e.g.: * [ ] Create tag for release e.g.:
@ -39,9 +57,8 @@ Released as needed for security, installation or critical bug fixes.
```bash ```bash
git push git push
``` ```
* [ ] If this is a security fix: publish the [GitHub Security Advisory or Advisories](https://github.com/python-pillow/Pillow/security/advisories).
## Embargoed release ## Embargoed Release
Released as needed privately to individual vendors for critical security-related bug fixes. Released as needed privately to individual vendors for critical security-related bug fixes.
@ -65,7 +82,7 @@ Released as needed privately to individual vendors for critical security-related
git push origin 2.5.x git push origin 2.5.x
``` ```
## Publicize release ## Publicize Release
* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321 * [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321
@ -73,7 +90,7 @@ Released as needed privately to individual vendors for critical security-related
* [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes * [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes
## Docker images ## Docker Images
* [ ] Update Pillow in the Docker Images repository * [ ] Update Pillow in the Docker Images repository
```bash ```bash

View File

@ -1,3 +1,4 @@
#!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import sys import sys

View File

@ -1,4 +1,4 @@
Pillow tests Pillow Tests
============ ============
Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``. Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``.

View File

@ -1,19 +1,18 @@
#!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import sys from typing import Any, Callable
from collections.abc import Callable
from typing import Any
import pytest import pytest
from PIL import Image from PIL import Image
from .helper import is_win32
min_iterations = 100 min_iterations = 100
max_iterations = 10000 max_iterations = 10000
pytestmark = pytest.mark.skipif( pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
sys.platform.startswith("win32"), reason="requires Unix or macOS"
)
def _get_mem_usage() -> float: def _get_mem_usage() -> float:

View File

@ -1,11 +1,12 @@
from __future__ import annotations from __future__ import annotations
import sys
from io import BytesIO from io import BytesIO
import pytest import pytest
from PIL import Image, features from PIL import Image
from .helper import is_win32, skip_unless_feature
# Limits for testing the leak # Limits for testing the leak
mem_limit = 1024 * 1048576 mem_limit = 1024 * 1048576
@ -14,10 +15,8 @@ iterations = int((mem_limit / stack_size) * 2)
test_file = "Tests/images/rgb_trns_ycbc.jp2" test_file = "Tests/images/rgb_trns_ycbc.jp2"
pytestmark = [ pytestmark = [
pytest.mark.skipif( pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"),
sys.platform.startswith("win32"), reason="requires Unix or macOS" skip_unless_feature("jpg_2000"),
),
pytest.mark.skipif(not features.check("jpg_2000"), reason="jpg_2000 not available"),
] ]

View File

@ -9,6 +9,6 @@ from PIL import Image
def test_j2k_overflow(tmp_path: Path) -> None: def test_j2k_overflow(tmp_path: Path) -> None:
im = Image.new("RGBA", (1024, 131584)) im = Image.new("RGBA", (1024, 131584))
target = tmp_path / "temp.jpc" target = str(tmp_path / "temp.jpc")
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(target) im.save(target)

View File

@ -1,11 +1,10 @@
from __future__ import annotations from __future__ import annotations
import sys
from io import BytesIO from io import BytesIO
import pytest import pytest
from PIL import Image from .helper import hopper, is_win32
iterations = 5000 iterations = 5000
@ -14,14 +13,12 @@ iterations = 5000
When run on a system without the jpeg leak fixes, When run on a system without the jpeg leak fixes,
the valgrind runs look like this. the valgrind runs look like this.
valgrind --tool=massif python test-installed.py -s -v checks/check_jpeg_leaks.py valgrind --tool=massif python test-installed.py -s -v Tests/check_jpeg_leaks.py
""" """
pytestmark = pytest.mark.skipif( pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
sys.platform.startswith("win32"), reason="requires Unix or macOS"
)
""" """
pre patch: pre patch:
@ -115,10 +112,10 @@ standard_chrominance_qtable = (
), ),
) )
def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None: def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None:
with Image.open("Tests/images/hopper.ppm") as im: im = hopper("RGB")
for _ in range(iterations): for _ in range(iterations):
test_output = BytesIO() test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables) im.save(test_output, "JPEG", qtables=qtables)
def test_exif_leak() -> None: def test_exif_leak() -> None:
@ -176,12 +173,12 @@ def test_exif_leak() -> None:
0 +----------------------------------------------------------------------->Gi 0 +----------------------------------------------------------------------->Gi
0 11.33 0 11.33
""" """
im = hopper("RGB")
exif = b"12345678" * 4096 exif = b"12345678" * 4096
with Image.open("Tests/images/hopper.ppm") as im: for _ in range(iterations):
for _ in range(iterations): test_output = BytesIO()
test_output = BytesIO() im.save(test_output, "JPEG", exif=exif)
im.save(test_output, "JPEG", exif=exif)
def test_base_save() -> None: def test_base_save() -> None:
@ -210,7 +207,8 @@ def test_base_save() -> None:
| :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@:::
0 +----------------------------------------------------------------------->Gi 0 +----------------------------------------------------------------------->Gi
0 7.882""" 0 7.882"""
with Image.open("Tests/images/hopper.ppm") as im: im = hopper("RGB")
for _ in range(iterations):
test_output = BytesIO() for _ in range(iterations):
im.save(test_output, "JPEG") test_output = BytesIO()
im.save(test_output, "JPEG")

View File

@ -32,7 +32,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy
def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
f = tmp_path / "temp.png" f = str(tmp_path / "temp.png")
im = Image.new("L", (xdim, ydim), 0) im = Image.new("L", (xdim, ydim), 0)
im.save(f) im.save(f)

View File

@ -28,7 +28,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy
def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
dtype = np.uint8 dtype = np.uint8
a = np.zeros((xdim, ydim), dtype=dtype) a = np.zeros((xdim, ydim), dtype=dtype)
f = tmp_path / "temp.png" f = str(tmp_path / "temp.png")
im = Image.fromarray(a, "L") im = Image.fromarray(a, "L")
im.save(f) im.save(f)

View File

@ -3,25 +3,26 @@ from __future__ import annotations
import zlib import zlib
from io import BytesIO from io import BytesIO
import pytest
from PIL import Image, ImageFile, PngImagePlugin from PIL import Image, ImageFile, PngImagePlugin
TEST_FILE = "Tests/images/png_decompression_dos.png" TEST_FILE = "Tests/images/png_decompression_dos.png"
def test_ignore_dos_text(monkeypatch: pytest.MonkeyPatch) -> None: def test_ignore_dos_text() -> None:
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) ImageFile.LOAD_TRUNCATED_IMAGES = True
with Image.open(TEST_FILE) as im: try:
im = Image.open(TEST_FILE)
im.load() im.load()
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
assert isinstance(im, PngImagePlugin.PngImageFile) assert isinstance(im, PngImagePlugin.PngImageFile)
for s in im.text.values(): for s in im.text.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M" assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
for s in im.info.values(): for s in im.info.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M" assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
def test_dos_text() -> None: def test_dos_text() -> None:

View File

@ -1,29 +1,19 @@
from __future__ import annotations from __future__ import annotations
import platform
import sys import sys
from PIL import features from PIL import features
def test_wheel_modules() -> None: def test_wheel_modules() -> None:
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"} expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
if sys.platform == "win32": # tkinter is not available in cibuildwheel installed CPython on Windows
# tkinter is not available in cibuildwheel installed CPython on Windows try:
try: import tkinter
import tkinter
assert tkinter assert tkinter
except ImportError: except ImportError:
expected_modules.remove("tkinter")
# libavif is not available on Windows for ARM64 architectures
if platform.machine() == "ARM64":
expected_modules.remove("avif")
elif sys.platform == "ios":
# tkinter is not available on iOS
expected_modules.remove("tkinter") expected_modules.remove("tkinter")
assert set(features.get_supported_modules()) == expected_modules assert set(features.get_supported_modules()) == expected_modules
@ -37,6 +27,9 @@ def test_wheel_codecs() -> None:
def test_wheel_features() -> None: def test_wheel_features() -> None:
expected_features = { expected_features = {
"webp_anim",
"webp_mux",
"transp_webp",
"raqm", "raqm",
"fribidi", "fribidi",
"harfbuzz", "harfbuzz",
@ -47,9 +40,5 @@ def test_wheel_features() -> None:
if sys.platform == "win32": if sys.platform == "win32":
expected_features.remove("xcb") expected_features.remove("xcb")
elif sys.platform == "ios":
# Can't distribute raqm due to licensing, and there's no system version;
# fribidi and harfbuzz won't be available if raqm isn't available.
expected_features -= {"raqm", "fribidi", "harfbuzz"}
assert set(features.get_supported_features()) == expected_features assert set(features.get_supported_features()) == expected_features

View File

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

1
Tests/createfontdatachunk.py Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import base64 import base64

View File

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

View File

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

View File

@ -9,21 +9,18 @@ import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
import sysconfig
import tempfile import tempfile
from collections.abc import Sequence
from functools import lru_cache from functools import lru_cache
from io import BytesIO from io import BytesIO
from typing import Any, Callable
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
from PIL import Image, ImageFile, ImageMath, features from PIL import Image, ImageFile, ImageMath, features
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
uploader = None uploader = None
@ -55,8 +52,8 @@ def convert_to_comparable(
if a.mode == "P": if a.mode == "P":
new_a = Image.new("L", a.size) new_a = Image.new("L", a.size)
new_b = Image.new("L", b.size) new_b = Image.new("L", b.size)
new_a.putdata(a.get_flattened_data()) new_a.putdata(a.getdata())
new_b.putdata(b.get_flattened_data()) new_b.putdata(b.getdata())
elif a.mode == "I;16": elif a.mode == "I;16":
new_a = a.convert("I") new_a = a.convert("I")
new_b = b.convert("I") new_b = b.convert("I")
@ -99,14 +96,12 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -
def assert_image_equal_tofile( def assert_image_equal_tofile(
a: Image.Image, a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None
filename: str | Path,
msg: str | None = None,
mode: str | None = None,
) -> None: ) -> None:
with Image.open(filename) as im: with Image.open(filename) as img:
converted_im = im.convert(mode) if mode else im if mode:
assert_image_equal(a, converted_im, msg) img = img.convert(mode)
assert_image_equal(a, img, msg)
def assert_image_similar( def assert_image_similar(
@ -142,14 +137,21 @@ def assert_image_similar(
def assert_image_similar_tofile( def assert_image_similar_tofile(
a: Image.Image, a: Image.Image,
filename: str | Path, filename: str,
epsilon: float, epsilon: float,
msg: str | None = None, msg: str | None = None,
mode: str | None = None,
) -> None: ) -> None:
with Image.open(filename) as img: with Image.open(filename) as img:
if mode:
img = img.convert(mode)
assert_image_similar(a, img, epsilon, msg) assert_image_similar(a, img, epsilon, msg)
def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) == len(items), msg
def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None: def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) != len(items), msg assert items.count(items[0]) != len(items), msg
@ -163,25 +165,11 @@ def assert_tuple_approx_equal(
pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets))
def timeout_unless_slower_valgrind(timeout: float) -> pytest.MarkDecorator:
if "PILLOW_VALGRIND_TEST" in os.environ:
return pytest.mark.pil_noop_mark()
return pytest.mark.timeout(timeout)
def skip_unless_feature(feature: str) -> pytest.MarkDecorator: def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
reason = f"{feature} not available" reason = f"{feature} not available"
return pytest.mark.skipif(not features.check(feature), reason=reason) return pytest.mark.skipif(not features.check(feature), reason=reason)
def has_feature_version(feature: str, required: str) -> bool:
version = features.version(feature)
assert version is not None
version_required = parse_version(required)
version_available = parse_version(version)
return version_available >= version_required
def skip_unless_feature_version( def skip_unless_feature_version(
feature: str, required: str, reason: str | None = None feature: str, required: str, reason: str | None = None
) -> pytest.MarkDecorator: ) -> pytest.MarkDecorator:
@ -281,13 +269,17 @@ def _cached_hopper(mode: str) -> Image.Image:
im = hopper("L") im = hopper("L")
else: else:
im = hopper() im = hopper()
try: if mode.startswith("BGR;"):
im = im.convert(mode) with pytest.warns(DeprecationWarning):
except ImportError: im = im.convert(mode)
if mode == "LAB": else:
im = Image.open("Tests/images/hopper.Lab.tif") try:
else: im = im.convert(mode)
raise except ImportError:
if mode == "LAB":
im = Image.open("Tests/images/hopper.Lab.tif")
else:
raise
return im return im
@ -301,6 +293,16 @@ def djpeg_available() -> bool:
return False return False
def cjpeg_available() -> bool:
if shutil.which("cjpeg"):
try:
subprocess.check_call(["cjpeg", "-version"])
return True
except subprocess.CalledProcessError: # pragma: no cover
return False
return False
def netpbm_available() -> bool: def netpbm_available() -> bool:
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
@ -325,7 +327,16 @@ def magick_command() -> list[str] | None:
return None return None
def on_appveyor() -> bool:
return "APPVEYOR" in os.environ
def on_github_actions() -> bool:
return "GITHUB_ACTIONS" in os.environ
def on_ci() -> bool: def on_ci() -> bool:
# GitHub Actions and AppVeyor have "CI"
return "CI" in os.environ return "CI" in os.environ
@ -347,6 +358,10 @@ def is_pypy() -> bool:
return hasattr(sys, "pypy_translation_info") return hasattr(sys, "pypy_translation_info")
def is_mingw() -> bool:
return sysconfig.get_platform() == "mingw"
class CachedProperty: class CachedProperty:
def __init__(self, func: Callable[[Any], Any]) -> None: def __init__(self, func: Callable[[Any], Any]) -> None:
self.func = func self.func = func

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

View File

@ -0,0 +1,578 @@
<!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.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

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