Merge branch 'main' into font

This commit is contained in:
Andrew Murray 2023-12-04 22:25:58 +11:00
commit 9a6c47a9d2
32 changed files with 326 additions and 133 deletions

View File

@ -0,0 +1 @@
cibuildwheel==2.16.2

View File

@ -72,10 +72,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
id: install id: install
run: | run: |
7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\" choco install nasm --no-progress
echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.0.0.20230317 choco install ghostscript --version=10.0.0.20230317 --no-progress
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
# Install extra test images # Install extra test images
@ -167,7 +167,6 @@ jobs:
- name: Build Pillow - name: Build Pillow
run: | run: |
$FLAGS="-C raqm=vendor -C fribidi=vendor" $FLAGS="-C raqm=vendor -C fribidi=vendor"
if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS+=" -C imagequant=disable" }
cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ." cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ."
& $env:pythonLocation\python.exe selftest.py --installed & $env:pythonLocation\python.exe selftest.py --installed
shell: pwsh shell: pwsh
@ -209,47 +208,6 @@ jobs:
flags: GHA_Windows flags: GHA_Windows
name: ${{ runner.os }} Python ${{ matrix.python-version }} name: ${{ runner.os }} Python ${{ matrix.python-version }}
- name: Build wheel
id: wheel
if: "github.event_name != 'pull_request'"
run: |
mkdir fribidi
copy winbuild\build\bin\fribidi* fribidi
setlocal EnableDelayedExpansion
for %%f in (winbuild\build\license\*) do (
set x=%%~nf
rem Skip FriBiDi license, it is not included in the wheel.
set fribidi=!x:~0,7!
if NOT !fribidi!==fribidi (
rem Skip imagequant license, it is not included in the wheel.
set libimagequant=!x:~0,13!
if NOT !libimagequant!==libimagequant (
echo. >> LICENSE
echo ===== %%~nf ===== >> LICENSE
echo. >> LICENSE
type %%f >> LICENSE
)
)
)
for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT%
call winbuild\\build\\build_env.cmd
%pythonLocation%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor -C imagequant=disable .
shell: cmd
- name: Upload wheel
uses: actions/upload-artifact@v3
if: "github.event_name != 'pull_request'"
with:
name: ${{ steps.wheel.outputs.dist }}
path: "*.whl"
- name: Upload fribidi.dll
if: "github.event_name != 'pull_request' && matrix.python-version == 3.11"
uses: actions/upload-artifact@v3
with:
name: fribidi
path: fribidi\*
success: success:
permissions: permissions:
contents: none contents: none

View File

@ -16,13 +16,13 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds # Package versions for fresh source builds
FREETYPE_VERSION=2.13.2 FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.2.1 HARFBUZZ_VERSION=8.3.0
LIBPNG_VERSION=1.6.40 LIBPNG_VERSION=1.6.40
JPEGTURBO_VERSION=3.0.1 JPEGTURBO_VERSION=3.0.1
OPENJPEG_VERSION=2.5.0 OPENJPEG_VERSION=2.5.0
XZ_VERSION=5.4.5 XZ_VERSION=5.4.5
TIFF_VERSION=4.6.0 TIFF_VERSION=4.6.0
LCMS2_VERSION=2.15 LCMS2_VERSION=2.16
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then
GIFLIB_VERSION=5.1.4 GIFLIB_VERSION=5.1.4
else else

22
.github/workflows/wheels-test.ps1 vendored Normal file
View File

@ -0,0 +1,22 @@
param ([string]$venv, [string]$pillow="C:\pillow")
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
Set-PSDebug -Trace 1
if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
# unlike CPython, PyPy requires Visual C++ Redistributable to be installed
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri 'https://aka.ms/vs/15/release/vc_redist.x64.exe' -OutFile 'vc_redist.x64.exe'
C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null
}
$env:path += ";$pillow\winbuild\build\bin\"
& "$venv\Scripts\activate.ps1"
& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
cd $pillow
& python -VV
if (!$?) { exit $LASTEXITCODE }
& python selftest.py
if (!$?) { exit $LASTEXITCODE }
& python -m pytest -vx Tests\check_wheel.py
if (!$?) { exit $LASTEXITCODE }
& python -m pytest -vx Tests
if (!$?) { exit $LASTEXITCODE }

View File

@ -1,10 +1,6 @@
#!/bin/bash #!/bin/bash
set -e set -e
EXP_CODECS="jpg jpg_2000 libtiff zlib"
EXP_MODULES="freetype2 littlecms2 pil tkinter webp"
EXP_FEATURES="fribidi harfbuzz libjpeg_turbo raqm transp_webp webp_anim webp_mux xcb"
if [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then
brew install fribidi brew install fribidi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
@ -25,21 +21,5 @@ fi
# Runs tests # Runs tests
python3 selftest.py python3 selftest.py
python3 -m pytest Tests/check_wheel.py
python3 -m pytest python3 -m pytest
# Test against expected codecs, modules and features
codecs=$(python3 -c 'from PIL.features import *; print(" ".join(sorted(get_supported_codecs())))')
if [ "$codecs" != "$EXP_CODECS" ]; then
echo "Codecs should be: '$EXP_CODECS'; but are '$codecs'"
exit 1
fi
modules=$(python3 -c 'from PIL.features import *; print(" ".join(sorted(get_supported_modules())))')
if [ "$modules" != "$EXP_MODULES" ]; then
echo "Modules should be: '$EXP_MODULES'; but are '$modules'"
exit 1
fi
features=$(python3 -c 'from PIL.features import *; print(" ".join(sorted(get_supported_features())))')
if [ "$features" != "$EXP_FEATURES" ]; then
echo "Features should be: '$EXP_FEATURES'; but are '$features'"
exit 1
fi

View File

@ -3,14 +3,20 @@ name: Wheels
on: on:
push: push:
paths: paths:
- ".github/workflows/wheels*.yml" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py"
- "winbuild/fribidi.cmake"
tags: tags:
- "*" - "*"
pull_request: pull_request:
paths: paths:
- ".github/workflows/wheels*.yml" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py"
- "winbuild/fribidi.cmake"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -52,14 +58,17 @@ jobs:
with: with:
submodules: true submodules: true
- name: Build wheels - uses: actions/setup-python@v4
uses: pypa/cibuildwheel@v2.16.2
with: with:
output-dir: wheelhouse python-version: "3.x"
- name: Build wheels
run: |
python3 -m pip install -r .ci/requirements-cibw.txt
python3 -m cibuildwheel --output-dir wheelhouse
env: env:
CIBW_ARCHS: ${{ matrix.archs }} CIBW_ARCHS: ${{ matrix.archs }}
CIBW_BUILD: ${{ matrix.build }} CIBW_BUILD: ${{ matrix.build }}
CIBW_CONFIG_SETTINGS: raqm=enable raqm=vendor fribidi=vendor
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_SKIP: pp38-* CIBW_SKIP: pp38-*
@ -71,6 +80,102 @@ jobs:
name: dist name: dist
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
windows:
name: Windows ${{ matrix.arch }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- arch: x86
cibw_arch: x86
- arch: x64
cibw_arch: AMD64
- arch: ARM64
cibw_arch: ARM64
steps:
- uses: actions/checkout@v4
- name: Checkout extra test images
uses: actions/checkout@v4
with:
repository: python-pillow/test-images
path: Tests\test-images
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Prepare for build
run: |
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images
& python.exe -m pip install -r .ci/requirements-cibw.txt
# Cannot cross-compile FriBiDi (only used for tests)
$FLAGS = ("--no-imagequant", "--architecture=${{ matrix.arch }}")
if ('${{ matrix.arch }}' -eq 'ARM64') { $FLAGS += "--no-fribidi" }
& python.exe winbuild\build_prepare.py -v @FLAGS
shell: pwsh
- name: Build wheels
run: |
setlocal EnableDelayedExpansion
for %%f in (winbuild\build\license\*) do (
set x=%%~nf
rem Skip FriBiDi license, it is not included in the wheel.
set fribidi=!x:~0,7!
if NOT !fribidi!==fribidi (
rem Skip imagequant license, it is not included in the wheel.
set libimagequant=!x:~0,13!
if NOT !libimagequant!==libimagequant (
echo. >> LICENSE
echo ===== %%~nf ===== >> LICENSE
echo. >> LICENSE
type %%f >> LICENSE
)
)
)
call winbuild\\build\\build_env.cmd
%pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse
env:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow
-v C:\cibw:C:\cibw
-v %CD%\..\venv-test:%CD%\..\venv-test
-e CI -e GITHUB_ACTIONS
mcr.microsoft.com/windows/servercore:ltsc2022
powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test'
shell: cmd
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: dist
path: ./wheelhouse/*.whl
- name: Prepare to upload FriBiDi
if: "matrix.arch != 'ARM64'"
run: |
mkdir fribidi\${{ matrix.arch }}
copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }}
shell: cmd
- name: Upload fribidi.dll
if: "matrix.arch != 'ARM64'"
uses: actions/upload-artifact@v3
with:
name: fribidi
path: fribidi\*
sdist: sdist:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -93,7 +198,7 @@ jobs:
success: success:
permissions: permissions:
contents: none contents: none
needs: [build, sdist] needs: [build, windows, sdist]
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Wheels Successful name: Wheels Successful
steps: steps:

View File

@ -3,7 +3,6 @@ if: tag IS present OR type = api
env: env:
global: global:
- CIBW_ARCHS=aarch64 - CIBW_ARCHS=aarch64
- CIBW_CONFIG_SETTINGS="raqm=enable raqm=vendor fribidi=vendor"
- CIBW_SKIP=pp38-* - CIBW_SKIP=pp38-*
language: python language: python
@ -35,7 +34,7 @@ jobs:
- CIBW_BUILD="*musllinux*" - CIBW_BUILD="*musllinux*"
install: install:
- python3 -m pip install cibuildwheel==2.16.2 - python3 -m pip install -r .ci/requirements-cibw.txt
script: script:
- python3 -m cibuildwheel --output-dir wheelhouse - python3 -m cibuildwheel --output-dir wheelhouse

View File

@ -5,6 +5,15 @@ Changelog (Pillow)
10.2.0 (unreleased) 10.2.0 (unreleased)
------------------- -------------------
- Correct PDF palette size when saving #7555
[radarhere]
- Fixed closing file pointer with olefile 0.47 #7594
[radarhere]
- Raise ValueError when TrueType font size is not greater than zero #7584, #7587
[akx, radarhere]
- If absent, do not try to close fp when closing image #7557 - If absent, do not try to close fp when closing image #7557
[RaphaelVRossi, radarhere] [RaphaelVRossi, radarhere]

View File

@ -94,7 +94,6 @@ Released as needed privately to individual vendors for critical security-related
## Source and Binary Distributions ## Source and Binary Distributions
### macOS and Linux
* [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) * [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
```bash ```bash
@ -104,14 +103,6 @@ Released as needed privately to individual vendors for critical security-related
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases) * [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
and copy into `dist`. and copy into `dist`.
### Windows
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
```bash
gh run download --dir dist
# select dist-x.y.z
```
## Publicize Release ## Publicize Release
* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 * [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010

41
Tests/check_wheel.py Normal file
View File

@ -0,0 +1,41 @@
import sys
from PIL import features
def test_wheel_modules():
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
# tkinter is not available in cibuildwheel installed CPython on Windows
try:
import tkinter
assert tkinter
except ImportError:
expected_modules.remove("tkinter")
assert set(features.get_supported_modules()) == expected_modules
def test_wheel_codecs():
expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"}
assert set(features.get_supported_codecs()) == expected_codecs
def test_wheel_features():
expected_features = {
"webp_anim",
"webp_mux",
"transp_webp",
"raqm",
"fribidi",
"harfbuzz",
"libjpeg_turbo",
"xcb",
}
if sys.platform == "win32":
expected_features.remove("xcb")
assert set(features.get_supported_features()) == expected_features

View File

@ -5,6 +5,7 @@ Helper functions.
import logging import logging
import os import os
import shutil import shutil
import subprocess
import sys import sys
import sysconfig import sysconfig
import tempfile import tempfile
@ -258,11 +259,21 @@ def hopper(mode=None, cache={}):
def djpeg_available(): def djpeg_available():
return bool(shutil.which("djpeg")) if shutil.which("djpeg"):
try:
subprocess.check_call(["djpeg", "-version"])
return True
except subprocess.CalledProcessError: # pragma: no cover
return False
def cjpeg_available(): def cjpeg_available():
return bool(shutil.which("cjpeg")) if shutil.which("cjpeg"):
try:
subprocess.check_call(["cjpeg", "-version"])
return True
except subprocess.CalledProcessError: # pragma: no cover
return False
def netpbm_available(): def netpbm_available():

View File

@ -67,7 +67,7 @@ def test_quantize_no_dither():
def test_quantize_no_dither2(): def test_quantize_no_dither2():
im = Image.new("RGB", (9, 1)) im = Image.new("RGB", (9, 1))
im.putdata(list((p,) * 3 for p in range(0, 36, 4))) im.putdata([(p,) * 3 for p in range(0, 36, 4)])
palette = Image.new("P", (1, 1)) palette = Image.new("P", (1, 1))
data = (0, 0, 0, 32, 32, 32) data = (0, 0, 0, 32, 32, 32)

View File

@ -1073,3 +1073,9 @@ def test_raqm_missing_warning(monkeypatch):
"Raqm layout was requested, but Raqm is not available. " "Raqm layout was requested, but Raqm is not available. "
"Falling back to basic layout." "Falling back to basic layout."
) )
@pytest.mark.parametrize("size", [-1, 0])
def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size):
with pytest.raises(ValueError):
ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)

View File

@ -11,6 +11,10 @@ from .helper import assert_image_equal_tofile, skip_unless_feature
class TestImageGrab: class TestImageGrab:
@pytest.mark.skipif(
os.environ.get("USERNAME") == "ContainerAdministrator",
reason="can't grab screen when running in Docker",
)
@pytest.mark.skipif( @pytest.mark.skipif(
sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS"
) )

View File

@ -166,6 +166,12 @@ html_static_path = ["resources"]
# directly to the root of the documentation. # directly to the root of the documentation.
# html_extra_path = [] # html_extra_path = []
html_css_files = ["css/dark.css"]
html_js_files = [
"js/activate_tab.js",
]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format. # using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y' # html_last_updated_fmt = '%b %d, %Y'
@ -313,10 +319,6 @@ texinfo_documents = [
# texinfo_no_detailmenu = False # texinfo_no_detailmenu = False
def setup(app):
app.add_css_file("css/dark.css")
linkcheck_allowed_redirects = { linkcheck_allowed_redirects = {
r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*", r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*",
r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg",

View File

@ -1,6 +1,14 @@
Installation Installation
============ ============
.. raw:: html
<script>
document.addEventListener('DOMContentLoaded', function() {
activateTab(getOS());
});
</script>
Warnings Warnings
-------- --------
@ -87,11 +95,10 @@ and :pypi:`olefile` for Pillow to read FPX and MIC images::
.. tab:: Windows .. tab:: Windows
.. warning:: Pillow > 9.5.0 no longer includes 32-bit wheels. We provide Pillow binaries for Windows compiled for the matrix of supported
Pythons in the wheel format. These include x86, x86-64 and arm64 versions
We provide Pillow binaries for Windows compiled for the matrix of (with the exception of Python 3.8 on arm64). These binaries include support
supported Pythons in 64-bit versions in the wheel format. These binaries include for all optional libraries except libimagequant and libxcb. Raqm support
support for all optional libraries except libimagequant and libxcb. Raqm support
requires FriBiDi to be installed separately:: requires FriBiDi to be installed separately::
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
@ -168,7 +175,7 @@ Many of Pillow's features require external libraries:
* **littlecms** provides color management * **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
above uses liblcms2. Tested with **1.19** and **2.7-2.15**. above uses liblcms2. Tested with **1.19** and **2.7-2.16**.
* **libwebp** provides the WebP format. * **libwebp** provides the WebP format.

View File

@ -0,0 +1,36 @@
// Based on https://stackoverflow.com/a/38241481/724176
function getOS() {
const userAgent = window.navigator.userAgent,
platform = window.navigator.userAgentData?.platform || window.navigator.platform,
macosPlatforms = ["macOS", "Macintosh", "MacIntel", "MacPPC", "Mac68K"],
windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"];
if (macosPlatforms.includes(platform)) {
return "macOS";
} else if (windowsPlatforms.includes(platform)) {
return "Windows";
} else if (/Android/.test(userAgent)) {
return "Android";
} else if (/Linux/.test(platform)) {
return "Linux";
}
}
function activateTab(tabName) {
// Find all label elements with the specified tab name
const labels = document.querySelectorAll(".tab-label");
labels.forEach((label) => {
if (label.textContent == tabName) {
// Find the associated input element using the "for" attribute
const tabInputId = label.getAttribute("for");
const tabInput = document.getElementById(tabInputId);
// Check if the input element exists before attempting to set the "checked" attribute
if (tabInput) {
// Activate the tab by setting its "checked" attribute to true
tabInput.checked = true;
}
}
});
}

View File

@ -47,6 +47,12 @@ docs = [
"sphinx-removed-in", "sphinx-removed-in",
"sphinxext-opengraph", "sphinxext-opengraph",
] ]
fpx = [
"olefile",
]
mic = [
"olefile",
]
tests = [ tests = [
"check-manifest", "check-manifest",
"coverage", "coverage",
@ -59,6 +65,9 @@ tests = [
"pytest-cov", "pytest-cov",
"pytest-timeout", "pytest-timeout",
] ]
xmp = [
"defusedxml",
]
[project.urls] [project.urls]
Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst" Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst"
Documentation = "https://pillow.readthedocs.io" Documentation = "https://pillow.readthedocs.io"
@ -79,12 +88,15 @@ version = {attr = "PIL.__version__"}
[tool.cibuildwheel] [tool.cibuildwheel]
before-all = ".github/workflows/wheels-dependencies.sh" before-all = ".github/workflows/wheels-dependencies.sh"
build-verbosity = 1
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
test-command = "cd {project} && .github/workflows/wheels-test.sh" test-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests" test-extras = "tests"
[tool.ruff] [tool.ruff]
line-length = 88 line-length = 88
select = [ select = [
"C4", # flake8-comprehensions
"E", # pycodestyle errors "E", # pycodestyle errors
"EM", # flake8-errmsg "EM", # flake8-errmsg
"F", # pyflakes errors "F", # pyflakes errors

View File

@ -440,17 +440,17 @@ class pil_build_ext(build_ext):
# #
# add configured kits # add configured kits
for root_name, lib_name in dict( for root_name, lib_name in {
JPEG_ROOT="libjpeg", "JPEG_ROOT": "libjpeg",
JPEG2K_ROOT="libopenjp2", "JPEG2K_ROOT": "libopenjp2",
TIFF_ROOT=("libtiff-5", "libtiff-4"), "TIFF_ROOT": ("libtiff-5", "libtiff-4"),
ZLIB_ROOT="zlib", "ZLIB_ROOT": "zlib",
FREETYPE_ROOT="freetype2", "FREETYPE_ROOT": "freetype2",
HARFBUZZ_ROOT="harfbuzz", "HARFBUZZ_ROOT": "harfbuzz",
FRIBIDI_ROOT="fribidi", "FRIBIDI_ROOT": "fribidi",
LCMS_ROOT="lcms2", "LCMS_ROOT": "lcms2",
IMAGEQUANT_ROOT="libimagequant", "IMAGEQUANT_ROOT": "libimagequant",
).items(): }.items():
root = globals()[root_name] root = globals()[root_name]
if root is None and root_name in os.environ: if root is None and root_name in os.environ:

View File

@ -396,7 +396,7 @@ def _save(im, fp, filename, bitmap_header=True):
dpi = info.get("dpi", (96, 96)) dpi = info.get("dpi", (96, 96))
# 1 meter == 39.3701 inches # 1 meter == 39.3701 inches
ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi)) ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi)
stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3) stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
header = 40 # or 64 for OS/2 version 2 header = 40 # or 64 for OS/2 version 2

View File

@ -64,8 +64,6 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
d, e, o, a = self.tile[0] d, e, o, a = self.tile[0]
self.tile[0] = d, (0, 0) + self.size, o, a self.tile[0] = d, (0, 0) + self.size, o, a
return
# #
# -------------------------------------------------------------------- # --------------------------------------------------------------------

View File

@ -227,6 +227,7 @@ class FpxImageFile(ImageFile.ImageFile):
break # isn't really required break # isn't really required
self.stream = stream self.stream = stream
self._fp = self.fp
self.fp = None self.fp = None
def load(self): def load(self):

View File

@ -40,7 +40,7 @@ from enum import IntEnum
from pathlib import Path from pathlib import Path
try: try:
import defusedxml.ElementTree as ElementTree from defusedxml import ElementTree
except ImportError: except ImportError:
ElementTree = None ElementTree = None
@ -1160,7 +1160,7 @@ class Image:
if palette.mode != "P": if palette.mode != "P":
msg = "bad mode for palette image" msg = "bad mode for palette image"
raise ValueError(msg) raise ValueError(msg)
if self.mode != "RGB" and self.mode != "L": if self.mode not in {"RGB", "L"}:
msg = "only RGB or L mode images can be quantized to a palette" msg = "only RGB or L mode images can be quantized to a palette"
raise ValueError(msg) raise ValueError(msg)
im = self.im.convert("P", dither, palette.im) im = self.im.convert("P", dither, palette.im)

View File

@ -921,7 +921,7 @@ def floodfill(image, xy, value, border=None, thresh=0):
if border is None: if border is None:
fill = _color_diff(p, background) <= thresh fill = _color_diff(p, background) <= thresh
else: else:
fill = p != value and p != border fill = p not in (value, border)
if fill: if fill:
pixel[s, t] = value pixel[s, t] = value
new_edge.add((s, t)) new_edge.add((s, t))

View File

@ -198,6 +198,10 @@ class FreeTypeFont:
) -> None: ) -> None:
# FIXME: use service provider instead # FIXME: use service provider instead
if size <= 0:
msg = "font size must be greater than 0"
raise ValueError(msg)
self.path = font self.path = font
self.size = size self.size = size
self.index = index self.index = index
@ -800,6 +804,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
.. versionadded:: 4.2.0 .. versionadded:: 4.2.0
:return: A font object. :return: A font object.
:exception OSError: If the file could not be read. :exception OSError: If the file could not be read.
:exception ValueError: If the font size is not greater than zero.
""" """
def freetype(font): def freetype(font):

View File

@ -334,10 +334,7 @@ def _save(im, fp, filename):
if quality_layers is not None and not ( if quality_layers is not None and not (
isinstance(quality_layers, (list, tuple)) isinstance(quality_layers, (list, tuple))
and all( and all(
[ isinstance(quality_layer, (int, float)) for quality_layer in quality_layers
isinstance(quality_layer, (int, float))
for quality_layer in quality_layers
]
) )
): ):
msg = "quality_layers must be a sequence of numbers" msg = "quality_layers must be a sequence of numbers"

View File

@ -397,7 +397,7 @@ class JpegImageFile(ImageFile.ImageFile):
# self.__offset = self.fp.tell() # self.__offset = self.fp.tell()
break break
s = self.fp.read(1) s = self.fp.read(1)
elif i == 0 or i == 0xFFFF: elif i in {0, 0xFFFF}:
# padded marker or junk; move on # padded marker or junk; move on
s = b"\xff" s = b"\xff"
elif i == 0xFF00: # Skip extraneous data (escaped 0xFF) elif i == 0xFF00: # Skip extraneous data (escaped 0xFF)

View File

@ -66,6 +66,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self._n_frames = len(self.images) self._n_frames = len(self.images)
self.is_animated = self._n_frames > 1 self.is_animated = self._n_frames > 1
self.__fp = self.fp
self.seek(0) self.seek(0)
def seek(self, frame): def seek(self, frame):
@ -87,10 +88,12 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
return self.frame return self.frame
def close(self): def close(self):
self.__fp.close()
self.ole.close() self.ole.close()
super().close() super().close()
def __exit__(self, *args): def __exit__(self, *args):
self.__fp.close()
self.ole.close() self.ole.close()
super().__exit__() super().__exit__()

View File

@ -96,7 +96,7 @@ def _write_image(im, filename, existing_pdf, image_refs):
dict_obj["ColorSpace"] = [ dict_obj["ColorSpace"] = [
PdfParser.PdfName("Indexed"), PdfParser.PdfName("Indexed"),
PdfParser.PdfName("DeviceRGB"), PdfParser.PdfName("DeviceRGB"),
255, len(palette) // 3 - 1,
PdfParser.PdfBinary(palette), PdfParser.PdfBinary(palette),
] ]
procset = "ImageI" # indexed color procset = "ImageI" # indexed color

View File

@ -123,7 +123,7 @@ class SgiImageFile(ImageFile.ImageFile):
def _save(im, fp, filename): def _save(im, fp, filename):
if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L": if im.mode not in {"RGB", "RGBA", "L"}:
msg = "Unsupported SGI image mode" msg = "Unsupported SGI image mode"
raise ValueError(msg) raise ValueError(msg)
@ -155,7 +155,7 @@ def _save(im, fp, filename):
# Z Dimension: Number of channels # Z Dimension: Number of channels
z = len(im.mode) z = len(im.mode)
if dim == 1 or dim == 2: if dim in {1, 2}:
z = 1 z = 1
# assert we've got the right number of bands. # assert we've got the right number of bands.

View File

@ -427,7 +427,7 @@ def _populate():
TAGS_V2[k] = TagInfo(k, *v) TAGS_V2[k] = TagInfo(k, *v)
for group, tags in TAGS_V2_GROUPS.items(): for tags in TAGS_V2_GROUPS.values():
for k, v in tags.items(): for k, v in tags.items():
tags[k] = TagInfo(k, *v) tags[k] = TagInfo(k, *v)

View File

@ -279,10 +279,10 @@ DEPS = {
"libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"],
}, },
"lcms2": { "lcms2": {
"url": SF_PROJECTS + "/lcms/files/lcms/2.15/lcms2-2.15.tar.gz/download", "url": SF_PROJECTS + "/lcms/files/lcms/2.16/lcms2-2.16.tar.gz/download",
"filename": "lcms2-2.15.tar.gz", "filename": "lcms2-2.16.tar.gz",
"dir": "lcms2-2.15", "dir": "lcms2-2.16",
"license": "COPYING", "license": "LICENSE",
"patch": { "patch": {
r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": {
# default is /MD for x86 and /MT for x64, we need /MD always # default is /MD for x86 and /MT for x64, we need /MD always
@ -345,9 +345,9 @@ DEPS = {
"libs": [r"imagequant.lib"], "libs": [r"imagequant.lib"],
}, },
"harfbuzz": { "harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/8.2.1.zip", "url": "https://github.com/harfbuzz/harfbuzz/archive/8.3.0.zip",
"filename": "harfbuzz-8.2.1.zip", "filename": "harfbuzz-8.3.0.zip",
"dir": "harfbuzz-8.2.1", "dir": "harfbuzz-8.3.0",
"license": "COPYING", "license": "COPYING",
"build": [ "build": [
*cmds_cmake( *cmds_cmake(
@ -482,7 +482,7 @@ def extract_dep(url: str, filename: str) -> None:
msg = "Attempted Path Traversal in Zip File" msg = "Attempted Path Traversal in Zip File"
raise RuntimeError(msg) raise RuntimeError(msg)
zf.extractall(sources_dir) zf.extractall(sources_dir)
elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): elif filename.endswith((".tar.gz", ".tgz")):
with tarfile.open(file, "r:gz") as tgz: with tarfile.open(file, "r:gz") as tgz:
for member in tgz.getnames(): for member in tgz.getnames():
member_abspath = os.path.abspath(os.path.join(sources_dir, member)) member_abspath = os.path.abspath(os.path.join(sources_dir, member))
@ -586,14 +586,19 @@ def build_dep(name: str) -> str:
def build_dep_all() -> None: def build_dep_all() -> None:
lines = [r'call "{build_dir}\build_env.cmd"'] lines = [r'call "{build_dir}\build_env.cmd"']
gha_groups = "GITHUB_ACTIONS" in os.environ
for dep_name in DEPS: for dep_name in DEPS:
print() print()
if dep_name in disabled: if dep_name in disabled:
print(f"Skipping disabled dependency {dep_name}") print(f"Skipping disabled dependency {dep_name}")
continue continue
script = build_dep(dep_name) script = build_dep(dep_name)
if gha_groups:
lines.append(f"@echo ::group::Running {script}")
lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"')
lines.append("if errorlevel 1 echo Build failed! && exit /B 1") lines.append("if errorlevel 1 echo Build failed! && exit /B 1")
if gha_groups:
lines.append("@echo ::endgroup::")
print() print()
lines.append("@echo All Pillow dependencies built successfully!") lines.append("@echo All Pillow dependencies built successfully!")
write_script("build_dep_all.cmd", lines) write_script("build_dep_all.cmd", lines)