Merge branch 'main' into jxl-support2

This commit is contained in:
Andrew Murray 2025-06-25 22:26:15 +10:00 committed by GitHub
commit ece406575c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
100 changed files with 1227 additions and 535 deletions

View File

@ -66,7 +66,7 @@ if [[ $(uname) != CYGWIN* ]]; then
pushd depends && ./install_raqm.sh && popd
# libavif
pushd depends && CMAKE_POLICY_VERSION_MINIMUM=3.5 ./install_libavif.sh && popd
pushd depends && ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -1 +1 @@
cibuildwheel==2.23.2
cibuildwheel==3.0.0

View File

@ -1,9 +1,10 @@
mypy==1.15.0
mypy==1.16.1
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython
numpy
packaging
pyarrow-stubs
pytest
sphinx
types-atheris

46
.github/ISSUE_TEMPLATE/RELEASE.md vendored Normal file
View File

@ -0,0 +1,46 @@
---
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]]
```

View File

@ -0,0 +1,60 @@
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.yml"
# - "**.c"
# - "**.h"
pull_request:
paths:
- ".github/workflows/test-valgrind.yml"
- "**.c"
- "**.h"
- "depends/docker-test-valgrind-memory.sh"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
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@v4
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

@ -31,16 +31,15 @@ env:
jobs:
build:
runs-on: ${{ matrix.os }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"]
architecture: ["x64"]
os: ["windows-latest"]
include:
# Test the oldest Python on 32-bit
- { python-version: "3.9", architecture: "x86", os: "windows-2019" }
- { python-version: "3.9", architecture: "x86" }
timeout-minutes: 45
@ -84,7 +83,7 @@ jobs:
python3 -m pip install --upgrade pip
- name: Install CPython dependencies
if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'"
if: "!contains(matrix.python-version, 'pypy') && !contains(matrix.python-version, '3.14') && matrix.architecture != 'x86'"
run: |
python3 -m pip install PyQt6
@ -98,8 +97,8 @@ jobs:
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.5.0 --no-progress
echo "C:\Program Files\gs\gs10.05.0\bin" >> $env:GITHUB_PATH
choco install ghostscript --version=10.5.1 --no-progress
echo "C:\Program Files\gs\gs10.05.1\bin" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images

View File

@ -43,6 +43,7 @@ jobs:
python-version: [
"pypy3.11",
"pypy3.10",
"3.14t",
"3.14",
"3.13t",
"3.13",
@ -55,6 +56,7 @@ jobs:
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
# Free-threaded
- { python-version: "3.14t", disable-gil: true }
- { python-version: "3.13t", disable-gil: true }
# M1 only available for 3.10+
- { os: "macos-13", python-version: "3.9" }

View File

@ -38,9 +38,9 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=11.1.0
LIBPNG_VERSION=1.6.47
JPEGTURBO_VERSION=3.1.0
HARFBUZZ_VERSION=11.2.1
LIBPNG_VERSION=1.6.49
JPEGTURBO_VERSION=3.1.1
OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.8.1
TIFF_VERSION=4.7.0

View File

@ -9,17 +9,21 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null
}
$env:path += ";$pillow\winbuild\build\bin\"
& "$venv\Scripts\activate.ps1"
if (Test-Path $venv\Scripts\pypy.exe) {
$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
if ("$venv" -like "*\cibw-run-*-win_amd64\*") {
& python -m pip install numpy
& $venv\Scripts\$python -m pip install numpy
}
cd $pillow
& python -VV
& $venv\Scripts\$python -VV
if (!$?) { exit $LASTEXITCODE }
& python selftest.py
& $venv\Scripts\$python selftest.py
if (!$?) { exit $LASTEXITCODE }
& python -m pytest -vx Tests\check_wheel.py
& $venv\Scripts\$python -m pytest -vx Tests\check_wheel.py
if (!$?) { exit $LASTEXITCODE }
& python -m pytest -vx Tests
& $venv\Scripts\$python -m pytest -vx Tests
if (!$?) { exit $LASTEXITCODE }

View File

@ -58,7 +58,7 @@ jobs:
- name: "macOS 10.13 x86_64"
os: macos-13
cibw_arch: x86_64
build: "cp3{12,13}*"
build: "cp3{12,13,14}*"
macosx_deployment_target: "10.13"
- name: "macOS 10.15 x86_64"
os: macos-13
@ -110,7 +110,6 @@ jobs:
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }}
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 }}
- uses: actions/upload-artifact@v4
@ -188,7 +187,6 @@ jobs:
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
CIBW_SKIP: pp39-*
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow

7
.github/zizmor.yml vendored Normal file
View File

@ -0,0 +1,7 @@
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
# https://woodruffw.github.io/zizmor/configuration/
rules:
unpinned-uses:
config:
policies:
"*": ref-pin

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.4
rev: v0.11.12
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v20.1.0
rev: v20.1.5
hooks:
- id: clang-format
types: [c]
@ -51,14 +51,14 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.32.1
rev: 0.33.0
hooks:
- id: check-github-workflows
- id: check-readthedocs
- id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.5.2
rev: v1.9.0
hooks:
- id: zizmor
@ -68,7 +68,7 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.5.1
rev: v2.6.0
hooks:
- id: pyproject-fmt

View File

@ -97,13 +97,27 @@ test:
python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
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
valgrind:
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
--log-file=/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
readme:
python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2

View File

@ -95,7 +95,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.
## More Information
## More information
- [Documentation](https://pillow.readthedocs.io/)
- [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html)
@ -107,6 +107,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)
- [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork)
## Report a Vulnerability
## Report a vulnerability
To report a security vulnerability, please follow the procedure described in the [Tidelift security policy](https://tidelift.com/docs/security).

View File

@ -1,34 +1,15 @@
# Release Checklist
# Release checklist
See https://pillow.readthedocs.io/en/stable/releasenotes/versioning.html for
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.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `main` branch.
* [ ] 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 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
* [ ] Create a new issue and select the "Maintainers only: Release" template.
## Point release
Released as needed for security, installation or critical bug fixes.
@ -58,7 +39,7 @@ Released as needed for security, installation or critical bug fixes.
git push
```
## Embargoed Release
## Embargoed release
Released as needed privately to individual vendors for critical security-related bug fixes.
@ -82,7 +63,7 @@ Released as needed privately to individual vendors for critical security-related
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
@ -90,7 +71,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
## Docker Images
## Docker images
* [ ] Update Pillow in the Docker Images repository
```bash

View File

@ -11,6 +11,7 @@ from .helper import is_pypy
def test_wheel_modules() -> None:
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
if sys.platform == "win32":
# tkinter is not available in cibuildwheel installed CPython on Windows
try:
import tkinter

View File

@ -161,6 +161,12 @@ def assert_tuple_approx_equal(
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:
reason = f"{feature} not available"
return pytest.mark.skipif(not features.check(feature), reason=reason)

BIN
Tests/images/op_index.qoi Normal file

Binary file not shown.

BIN
Tests/images/p_4_planes.pcx Normal file

Binary file not shown.

View File

@ -14,3 +14,23 @@
fun:_TIFFReadEncodedTileAndAllocBuffer
...
}
{
<python_alloc_possible_leak>
Memcheck:Leak
match-leak-kinds: all
fun:malloc
fun:_PyMem_RawMalloc
fun:PyObject_Malloc
...
}
{
<python_realloc_possible_leak>
Memcheck:Leak
match-leak-kinds: all
fun:malloc
fun:_PyMem_RawRealloc
fun:PyMem_Realloc
...
}

View File

@ -47,7 +47,6 @@ def test_unknown_version() -> None:
],
)
def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
expected = r""
with pytest.raises(RuntimeError, match=expected):
_deprecate.deprecate(deprecated, 1, plural=plural)

View File

@ -233,7 +233,7 @@ class TestFileAvif:
with Image.open(out_gif) as reread:
reread_value = reread.convert("RGB").getpixel((1, 1))
difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)])
assert difference <= 3
assert difference <= 6
def test_save_single_frame(self, tmp_path: Path) -> None:
temp_file = tmp_path / "temp.avif"

View File

@ -7,9 +7,8 @@ import pytest
from PIL import BlpImagePlugin, Image
from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar,
assert_image_similar_tofile,
hopper,
)
@ -52,18 +51,16 @@ def test_save(tmp_path: Path) -> None:
im = hopper("P")
im.save(f, blp_version=version)
with Image.open(f) as reloaded:
assert_image_equal(im.convert("RGB"), reloaded)
assert_image_equal_tofile(im.convert("RGB"), f)
with Image.open("Tests/images/transparent.png") as im:
f = tmp_path / "temp.blp"
im.convert("P").save(f, blp_version=version)
with Image.open(f) as reloaded:
assert_image_similar(im, reloaded, 8)
assert_image_similar_tofile(im, f, 8)
im = hopper()
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="Unsupported BLP image mode"):
im.save(f)

View File

@ -15,6 +15,7 @@ from .helper import (
is_win32,
mark_if_feature_version,
skip_unless_feature,
timeout_unless_slower_valgrind,
)
HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript()
@ -398,7 +399,7 @@ def test_emptyline() -> None:
assert image.format == "EPS"
@pytest.mark.timeout(timeout=5)
@timeout_unless_slower_valgrind(5)
@pytest.mark.parametrize(
"test_file",
["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],

View File

@ -7,7 +7,12 @@ import pytest
from PIL import FliImagePlugin, Image, ImageFile
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
from .helper import (
assert_image_equal,
assert_image_equal_tofile,
is_pypy,
timeout_unless_slower_valgrind,
)
# created as an export of a palette image from Gimp2.6
# save as...-> hopper.fli, default options.
@ -189,7 +194,7 @@ def test_seek() -> None:
"Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli",
],
)
@pytest.mark.timeout(timeout=3)
@timeout_unless_slower_valgrind(3)
def test_timeouts(test_file: str) -> None:
with open(test_file, "rb") as f:
with Image.open(f) as im:

View File

@ -32,6 +32,7 @@ from .helper import (
is_win32,
mark_if_feature_version,
skip_unless_feature,
timeout_unless_slower_valgrind,
)
ElementTree: ModuleType | None
@ -144,14 +145,16 @@ class TestFileJpeg:
assert k > 0.9
# roundtrip, and check again
im = self.roundtrip(im)
c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0)))
cmyk = im.getpixel((0, 0))
assert isinstance(cmyk, tuple)
c, m, y, k = (x / 255.0 for x in cmyk)
assert c == 0.0
assert m > 0.8
assert y > 0.8
assert k == 0.0
c, m, y, k = (
x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1))
)
cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1))
assert isinstance(cmyk, tuple)
k = cmyk[3] / 255.0
assert k > 0.9
def test_rgb(self) -> None:
@ -1033,7 +1036,7 @@ class TestFileJpeg:
with pytest.raises(ValueError):
im.save(f, xmp=b"1" * 65505)
@pytest.mark.timeout(timeout=1)
@timeout_unless_slower_valgrind(1)
def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Even though this decoder never says that it is finished
# the image should still end when there is no new data
@ -1064,10 +1067,16 @@ class TestFileJpeg:
for marker in b"\xff\xd8", b"\xff\xd9":
assert marker in data[1]
assert marker in data[2]
# DHT, DQT
for marker in b"\xff\xc4", b"\xff\xdb":
# DQT
markers = [b"\xff\xdb"]
if features.check_feature("libjpeg_turbo"):
# DHT
markers.append(b"\xff\xc4")
for marker in markers:
assert marker in data[1]
assert marker not in data[2]
# SOF0, SOS, APP0 (JFIF header)
for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0":
assert marker not in data[1]

View File

@ -37,6 +37,11 @@ def test_sanity(tmp_path: Path) -> None:
im.save(f)
def test_p_4_planes() -> None:
with Image.open("Tests/images/p_4_planes.pcx") as im:
assert im.getpixel((0, 0)) == 3
def test_bad_image_size() -> None:
with open("Tests/images/pil184.pcx", "rb") as fp:
data = fp.read()

View File

@ -13,7 +13,12 @@ import pytest
from PIL import Image, PdfParser, features
from .helper import hopper, mark_if_feature_version, skip_unless_feature
from .helper import (
hopper,
mark_if_feature_version,
skip_unless_feature,
timeout_unless_slower_valgrind,
)
def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str:
@ -339,8 +344,7 @@ def test_pdf_append_to_bytesio() -> None:
assert len(f.getvalue()) > initial_size
@pytest.mark.timeout(1)
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
@timeout_unless_slower_valgrind(1)
@pytest.mark.parametrize("newline", (b"\r", b"\n"))
def test_redos(newline: bytes) -> None:
malicious = b" trailer<<>>" + newline * 3456

View File

@ -100,11 +100,11 @@ class TestFilePng:
assert im.format == "PNG"
assert im.get_format_mimetype() == "image/png"
for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]:
for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]:
im = hopper(mode)
im.save(test_file)
with Image.open(test_file) as reloaded:
if mode in ("I", "I;16B"):
if mode == "I;16B":
reloaded = reloaded.convert(mode)
assert_image_equal(reloaded, im)
@ -801,6 +801,16 @@ class TestFilePng:
with Image.open("Tests/images/truncated_end_chunk.png") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.png")
def test_deprecation(self, tmp_path: Path) -> None:
test_file = tmp_path / "out.png"
im = hopper("I")
with pytest.warns(DeprecationWarning):
im.save(test_file)
with Image.open(test_file) as reloaded:
assert_image_equal(im, reloaded.convert("I"))
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
@skip_unless_feature("zlib")

View File

@ -288,14 +288,16 @@ def test_non_integer_token(tmp_path: Path) -> None:
pass
def test_header_token_too_long(tmp_path: Path) -> None:
@pytest.mark.parametrize("data", (b"P3\x0cAAAAAAAAAA\xee", b"P6\n 01234567890"))
def test_header_token_too_long(tmp_path: Path, data: bytes) -> None:
path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
f.write(b"P6\n 01234567890")
f.write(data)
with pytest.raises(ValueError, match="Token too long in file header: 01234567890"):
with pytest.raises(ValueError) as e:
with Image.open(path):
pass
assert "Token too long in file header: " in repr(e)
def test_truncated_file(tmp_path: Path) -> None:

View File

@ -1,10 +1,12 @@
from __future__ import annotations
from pathlib import Path
import pytest
from PIL import Image, QoiImagePlugin
from .helper import assert_image_equal_tofile
from .helper import assert_image_equal_tofile, hopper
def test_sanity() -> None:
@ -28,3 +30,28 @@ def test_invalid_file() -> None:
with pytest.raises(SyntaxError):
QoiImagePlugin.QoiImageFile(invalid_file)
def test_op_index() -> None:
# QOI_OP_INDEX as the first chunk
with Image.open("Tests/images/op_index.qoi") as im:
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
def test_save(tmp_path: Path) -> None:
f = tmp_path / "temp.qoi"
im = hopper()
im.save(f, colorspace="sRGB")
assert_image_equal_tofile(im, f)
for path in ("Tests/images/default_font.png", "Tests/images/pil123rgba.png"):
with Image.open(path) as im:
im.save(f)
assert_image_equal_tofile(im, f)
im = hopper("P")
with pytest.raises(ValueError, match="Unsupported QOI image mode"):
im.save(f)

View File

@ -14,6 +14,7 @@ from PIL import (
ImageFile,
JpegImagePlugin,
TiffImagePlugin,
TiffTags,
UnidentifiedImageError,
)
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
@ -26,6 +27,7 @@ from .helper import (
hopper,
is_pypy,
is_win32,
timeout_unless_slower_valgrind,
)
ElementTree: ModuleType | None
@ -47,23 +49,8 @@ class TestFileTiff:
assert im.size == (128, 128)
assert im.format == "TIFF"
hopper("1").save(filename)
with Image.open(filename):
pass
hopper("L").save(filename)
with Image.open(filename):
pass
hopper("P").save(filename)
with Image.open(filename):
pass
hopper("RGB").save(filename)
with Image.open(filename):
pass
hopper("I").save(filename)
for mode in ("1", "L", "P", "RGB", "I", "I;16", "I;16L"):
hopper(mode).save(filename)
with Image.open(filename):
pass
@ -899,6 +886,29 @@ class TestFileTiff:
assert description[0]["format"] == "image/tiff"
assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"]
def test_getxmp_undefined(self, tmp_path: Path) -> None:
tmpfile = tmp_path / "temp.tif"
im = Image.new("L", (1, 1))
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd.tagtype[700] = TiffTags.UNDEFINED
with Image.open("Tests/images/lab.tif") as im_xmp:
ifd[700] = im_xmp.info["xmp"]
im.save(tmpfile, tiffinfo=ifd)
with Image.open(tmpfile) as im_reloaded:
if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im_reloaded.getxmp() == {}
else:
assert "xmp" in im_reloaded.info
xmp = im_reloaded.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]
assert description[0]["format"] == "image/tiff"
def test_get_photoshop_blocks(self) -> None:
with Image.open("Tests/images/lab.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
@ -988,7 +998,7 @@ class TestFileTiff:
with pytest.raises(OSError):
im.load()
@pytest.mark.timeout(6)
@timeout_unless_slower_valgrind(6)
@pytest.mark.filterwarnings("ignore:Truncated File Read")
def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/timeout-6646305047838720") as im:
@ -1001,7 +1011,7 @@ class TestFileTiff:
"Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif",
],
)
@pytest.mark.timeout(2)
@timeout_unless_slower_valgrind(2)
def test_oom(self, test_file: str) -> None:
with pytest.raises(UnidentifiedImageError):
with pytest.warns(UserWarning):

View File

@ -34,6 +34,7 @@ from .helper import (
is_win32,
mark_if_feature_version,
skip_unless_feature,
timeout_unless_slower_valgrind,
)
ElementTree: ModuleType | None
@ -572,10 +573,7 @@ class TestImage:
i = Image.new("RGB", [1, 1])
assert isinstance(i.size, tuple)
@pytest.mark.timeout(0.75)
@pytest.mark.skipif(
"PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower"
)
@timeout_unless_slower_valgrind(0.75)
@pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0)))
def test_empty_image(self, size: tuple[int, int]) -> None:
Image.new("RGB", size)
@ -673,6 +671,7 @@ class TestImage:
im_remapped = im.remap_palette(list(range(256)))
assert_image_equal(im, im_remapped)
assert im.palette is not None
assert im_remapped.palette is not None
assert im.palette.palette == im_remapped.palette.palette
# Test illegal image mode
@ -975,6 +974,11 @@ class TestImage:
assert tag not in exif.get_ifd(0x8769)
assert exif.get_ifd(0xA005)
def test_exif_from_xmp_bytes(self) -> None:
im = Image.new("RGB", (1, 1))
im.info["xmp"] = b'\xff tiff:Orientation="2"'
assert im.getexif()[274] == 2
def test_empty_xmp(self) -> None:
with Image.open("Tests/images/hopper.gif") as im:
if ElementTree is None:

View File

@ -101,6 +101,7 @@ def test_fromarray_strides_without_tobytes() -> None:
with pytest.raises(ValueError):
wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)})
with pytest.warns(DeprecationWarning):
Image.fromarray(wrapped, "L")
@ -110,6 +111,7 @@ def test_fromarray_palette() -> None:
a = numpy.array(i)
# Act
with pytest.warns(DeprecationWarning):
out = Image.fromarray(a, "P")
# Assert that the Python and C palettes match

View File

@ -462,7 +462,7 @@ class TestCoreResampleBox:
im.resize((32, 32), resample, (20, 20, 20, 100))
im.resize((32, 32), resample, (20, 20, 100, 20))
with pytest.raises(TypeError, match="must be sequence of length 4"):
with pytest.raises(TypeError, match="must be (sequence|tuple) of length 4"):
im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type]
with pytest.raises(ValueError, match="can't be negative"):

View File

@ -783,9 +783,10 @@ def test_rectangle_I16(bbox: Coords) -> None:
draw = ImageDraw.Draw(im)
# Act
draw.rectangle(bbox, outline=0xFFFF)
draw.rectangle(bbox, outline=0xCDEF)
# Assert
assert im.getpixel((X0, Y0)) == 0xCDEF
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff")

View File

@ -7,7 +7,7 @@ import pytest
from PIL import Image, ImageDraw, ImageFont, _util, features
from .helper import assert_image_equal_tofile
from .helper import assert_image_equal_tofile, timeout_unless_slower_valgrind
fonts = [ImageFont.load_default_imagefont()]
if not features.check_module("freetype2"):
@ -72,7 +72,7 @@ def test_decompression_bomb() -> None:
font.getmask("A" * 1_000_000)
@pytest.mark.timeout(4)
@timeout_unless_slower_valgrind(4)
def test_oom() -> None:
glyph = struct.pack(
">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767

View File

@ -162,3 +162,13 @@ def test_pickle_font_file(tmp_path: Path, protocol: int) -> None:
# Assert
helper_assert_pickled_font_images(font, unpickled_font)
def test_load_earlier_data() -> None:
im = pickle.loads(
b"\x80\x04\x95@\x00\x00\x00\x00\x00\x00\x00\x8c\x12PIL.PngImagePlugin"
b"\x94\x8c\x0cPngImageFile\x94\x93\x94)\x81\x94]\x94(}\x94\x8c\x01L\x94K\x01"
b"K\x01\x86\x94NC\x01\x00\x94eb."
)
assert im.mode == "L"
assert im.size == (1, 1)

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Any # undone
from typing import Any, NamedTuple
import pytest
@ -10,30 +10,73 @@ from .helper import (
assert_deep_equal,
assert_image_equal,
hopper,
is_big_endian,
)
TYPE_CHECKING = False
if TYPE_CHECKING:
import pyarrow
else:
pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed")
TEST_IMAGE_SIZE = (10, 10)
def _test_img_equals_pyarray(
img: Image.Image, arr: Any, mask: list[int] | None
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
) -> None:
assert img.height * img.width == len(arr)
assert img.height * img.width * elts_per_pixel == len(arr)
px = img.load()
assert px is not None
if elts_per_pixel > 1 and mask is None:
# have to do element-wise comparison when we're comparing
# flattened r,g,b,a to a pixel.
mask = list(range(elts_per_pixel))
for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)):
if mask:
for ix, elt in enumerate(mask):
pixel = px[x, y]
assert isinstance(pixel, tuple)
for ix, elt in enumerate(mask):
if elts_per_pixel == 1:
assert pixel[ix] == arr[y * img.width + x].as_py()[elt]
else:
assert (
pixel[ix]
== arr[(y * img.width + x) * elts_per_pixel + elt].as_py()
)
else:
assert_deep_equal(px[x, y], arr[y * img.width + x].as_py())
def _test_img_equals_int32_pyarray(
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
) -> None:
assert img.height * img.width * elts_per_pixel == len(arr)
px = img.load()
assert px is not None
if mask is None:
# have to do element-wise comparison when we're comparing
# flattened rgba in an uint32 to a pixel.
mask = list(range(elts_per_pixel))
for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)):
pixel = px[x, y]
assert isinstance(pixel, tuple)
arr_pixel_int = arr[y * img.width + x].as_py()
arr_pixel_tuple = (
arr_pixel_int % 256,
(arr_pixel_int // 256) % 256,
(arr_pixel_int // 256**2) % 256,
(arr_pixel_int // 256**3),
)
if is_big_endian():
arr_pixel_tuple = arr_pixel_tuple[::-1]
for ix, elt in enumerate(mask):
assert pixel[ix] == arr_pixel_tuple[elt]
# really hard to get a non-nullable list type
fl_uint8_4_type = pyarrow.field(
"_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4)
@ -55,14 +98,14 @@ fl_uint8_4_type = pyarrow.field(
("HSV", fl_uint8_4_type, [0, 1, 2]),
),
)
def test_to_array(mode: str, dtype: Any, mask: list[int] | None) -> None:
def test_to_array(mode: str, dtype: pyarrow.DataType, mask: list[int] | None) -> None:
img = hopper(mode)
# Resize to non-square
img = img.crop((3, 0, 124, 127))
assert img.size == (121, 127)
arr = pyarrow.array(img)
arr = pyarrow.array(img) # type: ignore[call-overload]
_test_img_equals_pyarray(img, arr, mask)
assert arr.type == dtype
@ -79,8 +122,8 @@ def test_lifetime() -> None:
img = hopper("L")
arr_1 = pyarrow.array(img)
arr_2 = pyarrow.array(img)
arr_1 = pyarrow.array(img) # type: ignore[call-overload]
arr_2 = pyarrow.array(img) # type: ignore[call-overload]
del img
@ -97,8 +140,8 @@ def test_lifetime2() -> None:
img = hopper("L")
arr_1 = pyarrow.array(img)
arr_2 = pyarrow.array(img)
arr_1 = pyarrow.array(img) # type: ignore[call-overload]
arr_2 = pyarrow.array(img) # type: ignore[call-overload]
assert arr_1.sum().as_py() > 0
del arr_1
@ -110,3 +153,94 @@ def test_lifetime2() -> None:
px = img2.load()
assert px # make mypy happy
assert isinstance(px[0, 0], int)
class DataShape(NamedTuple):
dtype: pyarrow.DataType
# Strictly speaking, elt should be a pixel or pixel component, so
# list[uint8][4], float, int, uint32, uint8, etc. But more
# correctly, it should be exactly the dtype from the line above.
elt: Any
elts_per_pixel: int
UINT_ARR = DataShape(
dtype=fl_uint8_4_type,
elt=[1, 2, 3, 4], # array of 4 uint8 per pixel
elts_per_pixel=1, # only one array per pixel
)
UINT = DataShape(
dtype=pyarrow.uint8(),
elt=3, # one uint8,
elts_per_pixel=4, # but repeated 4x per pixel
)
UINT32 = DataShape(
dtype=pyarrow.uint32(),
elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000
elts_per_pixel=1, # one per pixel
)
INT32 = DataShape(
dtype=pyarrow.uint32(),
elt=0x12CDEF45, # one packed int
elts_per_pixel=1, # one per pixel
)
@pytest.mark.parametrize(
"mode, data_tp, mask",
(
("L", DataShape(pyarrow.uint8(), 3, 1), None),
("I", DataShape(pyarrow.int32(), 1 << 24, 1), None),
("F", DataShape(pyarrow.float32(), 3.14159, 1), None),
("LA", UINT_ARR, [0, 3]),
("LA", UINT, [0, 3]),
("RGB", UINT_ARR, [0, 1, 2]),
("RGBA", UINT_ARR, None),
("CMYK", UINT_ARR, None),
("YCbCr", UINT_ARR, [0, 1, 2]),
("HSV", UINT_ARR, [0, 1, 2]),
("RGB", UINT, [0, 1, 2]),
("RGBA", UINT, None),
("CMYK", UINT, None),
("YCbCr", UINT, [0, 1, 2]),
("HSV", UINT, [0, 1, 2]),
),
)
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
(dtype, elt, elts_per_pixel) = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
_test_img_equals_pyarray(img, arr, mask, elts_per_pixel)
@pytest.mark.parametrize(
"mode, data_tp, mask",
(
("LA", UINT32, [0, 3]),
("RGB", UINT32, [0, 1, 2]),
("RGBA", UINT32, None),
("CMYK", UINT32, None),
("YCbCr", UINT32, [0, 1, 2]),
("HSV", UINT32, [0, 1, 2]),
("LA", INT32, [0, 3]),
("RGB", INT32, [0, 1, 2]),
("RGBA", INT32, None),
("CMYK", INT32, None),
("YCbCr", INT32, [0, 1, 2]),
("HSV", INT32, [0, 1, 2]),
),
)
def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
(dtype, elt, elts_per_pixel) = data_tp
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
arr = pyarrow.array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
_test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel)

View File

@ -52,3 +52,17 @@ def test_tiff_crashes(test_file: str) -> None:
pytest.skip("test image not found")
except OSError:
pass
def test_tiff_mmap() -> None:
try:
with Image.open("Tests/images/crash_mmap.tif") as im:
im.seek(1)
im.load()
im.seek(0)
im.load()
except FileNotFoundError:
if on_ci():
raise
pytest.skip("test image not found")

View File

@ -0,0 +1,11 @@
#!/bin/bash
## Run this as the test script in the Docker valgrind image.
## Note -- can be included directly into the Docker image,
## but requires the current python.supp.
source /vpy3/bin/activate
cd /Pillow
make clean
make install
make valgrind-leak

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -eo pipefail
version=1.2.1
version=1.3.0
./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz

View File

@ -3,17 +3,23 @@
# You can set these variables from the command line.
PYTHON = python3
SPHINXOPTS =
SPHINXBUILD = $(PYTHON) -m sphinx.cmd.build
PAPER =
SPHINXOPTS = --fail-on-warning
BUILDDIR = _build
BUILDER = html
JOBS = auto
PAPER =
# Internal variables.
PAPEROPT_a4 = --define latex_paper_size=a4
PAPEROPT_letter = --define latex_paper_size=letter
ALLSPHINXOPTS = --doctree-dir $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
ALLSPHINXOPTS = --builder $(BUILDER) \
--doctree-dir $(BUILDDIR)/doctrees \
--jobs $(JOBS) \
$(PAPEROPT_$(PAPER)) \
$(SPHINXOPTS) \
. $(BUILDDIR)/$(BUILDER)
.PHONY: help
help:
@ -36,31 +42,19 @@ install-sphinx:
.PHONY: html
html:
$(MAKE) install-sphinx
$(SPHINXBUILD) --builder html --fail-on-warning --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
$(SPHINXBUILD) $(ALLSPHINXOPTS)
.PHONY: dirhtml
dirhtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) --builder dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
dirhtml: BUILDER = dirhtml
dirhtml: html
.PHONY: singlehtml
singlehtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) --builder singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
singlehtml: BUILDER = singlehtml
singlehtml: html
.PHONY: linkcheck
linkcheck:
$(MAKE) install-sphinx
$(SPHINXBUILD) --builder linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
linkcheck: BUILDER = linkcheck
linkcheck: html
.PHONY: htmlview
htmlview: html

View File

@ -193,6 +193,28 @@ Image.Image.get_child_images()
method uses an image's file pointer, and so child images could only be retrieved from
an :py:class:`PIL.ImageFile.ImageFile` instance.
Image.fromarray mode parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.3.0
The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The
mode can be automatically determined from the object's shape and type instead.
Saving I mode images as PNG
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.3.0
In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain
at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly
changing the data, this is now deprecated. Instead, the image can be converted to
another mode before saving::
from PIL import Image
im = Image.new("I", (1, 1))
im.convert("I;16").save("out.png")
Removed features
----------------

View File

@ -1082,6 +1082,26 @@ Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I
Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well.
QOI
^^^
.. versionadded:: 9.5.0
Pillow reads and writes images in Quite OK Image format using a Python codec. If you
wish to write code specifically for this format, :pypi:`qoi` is an alternative library
that uses C to decode the image and interfaces with NumPy.
.. _qoi-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
**colorspace**
If set to "sRGB", the colorspace will be written as sRGB with linear alpha, instead
of all channels being linear.
SGI
^^^
@ -1578,15 +1598,6 @@ PSD
Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
QOI
^^^
.. versionadded:: 9.5.0
Pillow reads images in Quite OK Image format using a Python decoder. If you wish to
write code specifically for this format, :pypi:`qoi` is an alternative library that
uses C to decode the image and interfaces with NumPy.
SUN
^^^

View File

@ -194,9 +194,9 @@ Many of Pillow's features require external libraries:
pacman -S \
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-python3 \
mingw-w64-x86_64-python3-pip \
mingw-w64-x86_64-python3-setuptools
mingw-w64-x86_64-python \
mingw-w64-x86_64-python-pip \
mingw-w64-x86_64-python-setuptools
Prerequisites are installed on **MSYS2 MinGW 64-bit** with::

View File

@ -40,18 +40,20 @@ These platforms are built and tested for every change.
| macOS 13 Ventura | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
| | PyPy3 | |
| | 3.14, PyPy3 | |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | |
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, |
| | | ppc64le, s390x |
| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, 3.14, PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 | arm64v8, ppc64le, |
| | | s390x |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2019 | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 |
| | PyPy3 | |
| Windows Server 2022 | 3.9 | x86 |
| +----------------------------+---------------------+
| | 3.10, 3.11, 3.12, 3.13, | x86-64 |
| | 3.14, PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 (MinGW) | x86-64 |
| +----------------------------+---------------------+

View File

@ -7,10 +7,8 @@ if "%SPHINXBUILD%" == "" (
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help

View File

@ -18,6 +18,9 @@ OpenType fonts (as well as other font formats supported by the FreeType
library). For earlier versions, TrueType support is only available as part of
the imToolkit package.
When measuring text sizes, this module will not break at newline characters. For
multiline text, see the :py:mod:`~PIL.ImageDraw` module.
.. warning::
To protect against potential DOS attacks when using arbitrary strings as
text input, Pillow will raise a :py:exc:`ValueError` if the number of characters

View File

@ -0,0 +1,89 @@
11.3.0
------
Security
========
TODO
^^^^
TODO
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards incompatible changes
==============================
TODO
^^^^
Deprecations
============
Image.fromarray mode parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The
mode can be automatically determined from the object's shape and type instead.
Saving I mode images as PNG
^^^^^^^^^^^^^^^^^^^^^^^^^^^
In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain
at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly
changing the data, this is now deprecated. Instead, the image can be converted to
another mode before saving::
from PIL import Image
im = Image.new("I", (1, 1))
im.convert("I;16").save("out.png")
API changes
===========
TODO
^^^^
TODO
API additions
=============
TODO
^^^^
TODO
Other changes
=============
Added QOI saving
^^^^^^^^^^^^^^^^
Support has been added for saving QOI images. ``colorspace`` can be used to specify the
colorspace as sRGB with linear alpha, e.g. ``im.save("out.qoi", colorspace="sRGB")``.
By default, all channels will be linear.
Support using more screenshot utilities with ImageGrab on Linux
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:py:meth:`~PIL.ImageGrab.grab` is now able to use GNOME Screenshot, grim or Spectacle
on Linux in order to take a snapshot of the screen.
Do not build against libavif < 1
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow only supports libavif 1.0.0 or later. In order to prevent errors when building
from source, if a user happens to have an earlier libavif on their system, Pillow will
now ignore it.
Python 3.14 beta
^^^^^^^^^^^^^^^^
To help other projects prepare for Python 3.14, wheels are now built for the
3.14 beta as a preview. This is not official support for Python 3.14, but rather
an opportunity for you to test how Pillow works with the beta and report any
problems.

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
11.3.0
11.2.1
11.1.0
11.0.0

View File

@ -70,6 +70,7 @@ optional-dependencies.tests = [
"pytest",
"pytest-cov",
"pytest-timeout",
"pytest-xdist",
"trove-classifiers>=2024.10.12",
]

View File

@ -16,7 +16,6 @@ import subprocess
import sys
import warnings
from collections.abc import Iterator
from typing import Any
from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext
@ -46,7 +45,7 @@ WEBP_ROOT = None
ZLIB_ROOT = None
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
if sys.platform == "win32" and sys.version_info >= (3, 14):
if sys.platform == "win32" and sys.version_info >= (3, 15):
import atexit
atexit.register(
@ -148,7 +147,7 @@ class RequiredDependencyException(Exception):
PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version
def _dbg(s: str, tp: Any = None) -> None:
def _dbg(s: str, tp: str | tuple[str, ...] | None = None) -> None:
if DEBUG:
if tp:
print(s % tp)
@ -163,7 +162,7 @@ def _find_library_dirs_ldconfig() -> list[str]:
args: list[str]
env: dict[str, str]
expr: str
if sys.platform.startswith("linux") or sys.platform.startswith("gnu"):
if sys.platform.startswith(("linux", "gnu")):
if struct.calcsize("l") == 4:
machine = os.uname()[4] + "-32"
else:
@ -224,13 +223,14 @@ def _add_directory(
path.insert(where, subdir)
def _find_include_file(self: pil_build_ext, include: str) -> int:
def _find_include_file(self: pil_build_ext, include: str) -> str | None:
for directory in self.compiler.include_dirs:
_dbg("Checking for include file %s in %s", (include, directory))
if os.path.isfile(os.path.join(directory, include)):
path = os.path.join(directory, include)
if os.path.isfile(path):
_dbg("Found %s", include)
return 1
return 0
return path
return None
def _find_library_file(self: pil_build_ext, library: str) -> str | None:
@ -509,11 +509,11 @@ class pil_build_ext(build_ext):
if root is None and pkg_config:
if isinstance(lib_name, str):
_dbg(f"Looking for `{lib_name}` using pkg-config.")
_dbg("Looking for `%s` using pkg-config.", lib_name)
root = pkg_config(lib_name)
else:
for lib_name2 in lib_name:
_dbg(f"Looking for `{lib_name2}` using pkg-config.")
_dbg("Looking for `%s` using pkg-config.", lib_name2)
root = pkg_config(lib_name2)
if root:
break
@ -623,11 +623,7 @@ class pil_build_ext(build_ext):
for extension in self.extensions:
extension.extra_compile_args = ["-Wno-nullability-completeness"]
elif (
sys.platform.startswith("linux")
or sys.platform.startswith("gnu")
or sys.platform.startswith("freebsd")
):
elif sys.platform.startswith(("linux", "gnu", "freebsd")):
for dirname in _find_library_dirs_ldconfig():
_add_directory(library_dirs, dirname)
if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"):
@ -736,7 +732,7 @@ class pil_build_ext(build_ext):
best_path = os.path.join(directory, name)
_dbg(
"Best openjpeg version %s so far in %s",
(best_version, best_path),
(str(best_version), best_path),
)
if best_version and _find_library_file(self, "openjp2"):
@ -766,12 +762,12 @@ class pil_build_ext(build_ext):
if feature.want("tiff"):
_dbg("Looking for tiff")
if _find_include_file(self, "tiff.h"):
if _find_library_file(self, "tiff"):
feature.set("tiff", "tiff")
if sys.platform in ["win32", "darwin"] and _find_library_file(
self, "libtiff"
):
feature.set("tiff", "libtiff")
elif _find_library_file(self, "tiff"):
feature.set("tiff", "tiff")
if feature.want("freetype"):
_dbg("Looking for freetype")
@ -861,8 +857,12 @@ class pil_build_ext(build_ext):
if feature.want("avif"):
_dbg("Looking for avif")
if _find_include_file(self, "avif/avif.h"):
if _find_library_file(self, "avif"):
if avif_h := _find_include_file(self, "avif/avif.h"):
with open(avif_h, "rb") as fp:
major_version = int(
fp.read().split(b"#define AVIF_VERSION_MAJOR ")[1].split()[0]
)
if major_version >= 1 and _find_library_file(self, "avif"):
feature.set("avif", "avif")
for f in feature:

View File

@ -16,7 +16,6 @@ except ImportError:
# Decoder options as module globals, until there is a way to pass parameters
# to Image.open (see https://github.com/python-pillow/Pillow/issues/569)
DECODE_CODEC_CHOICE = "auto"
# Decoding is only affected by this for libavif **0.8.4** or greater.
DEFAULT_MAX_THREADS = 0

View File

@ -1,5 +1,5 @@
"""
A Pillow loader for .dds files (S3TC-compressed aka DXTC)
A Pillow plugin for .dds files (S3TC-compressed aka DXTC)
Jerome Leclanche <jerome@leclan.ch>
Documentation:

View File

@ -31,7 +31,7 @@ import os
import subprocess
from enum import IntEnum
from functools import cached_property
from typing import IO, Any, Literal, NamedTuple, Union
from typing import IO, Any, Literal, NamedTuple, Union, cast
from . import (
Image,
@ -350,12 +350,15 @@ class GifImageFile(ImageFile.ImageFile):
if self._frame_palette:
if color * 3 + 3 > len(self._frame_palette.palette):
color = 0
return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
return cast(
tuple[int, int, int],
tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]),
)
else:
return (color, color, color)
self.dispose = None
self.dispose_extent = frame_dispose_extent
self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent
if self.dispose_extent and self.disposal_method >= 2:
try:
if self.disposal_method == 2:

View File

@ -767,18 +767,20 @@ class Image:
.. warning::
This method returns the raw image data from the internal
This method returns raw image data derived from Pillow's internal
storage. For compressed image data (e.g. PNG, JPEG) use
:meth:`~.save`, with a BytesIO parameter for in-memory
data.
:meth:`~.save`, with a BytesIO parameter for in-memory data.
:param encoder_name: What encoder to use. The default is to
use the standard "raw" encoder.
:param encoder_name: What encoder to use.
A list of C encoders can be seen under
codecs section of the function array in
:file:`_imaging.c`. Python encoders are
registered within the relevant plugins.
The default is to use the standard "raw" encoder.
To see how this packs pixel data into the returned
bytes, see :file:`libImaging/Pack.c`.
A list of C encoders can be seen under codecs
section of the function array in
:file:`_imaging.c`. Python encoders are registered
within the relevant plugins.
:param args: Extra arguments to the encoder.
:returns: A :py:class:`bytes` object.
"""
@ -800,7 +802,9 @@ class Image:
e = _getencoder(self.mode, encoder_name, encoder_args)
e.setimage(self.im)
bufsize = max(65536, self.size[0] * 4) # see RawEncode.c
from . import ImageFile
bufsize = max(ImageFile.MAXBLOCK, self.size[0] * 4) # see RawEncode.c
output = []
while True:
@ -1538,10 +1542,11 @@ class Image:
# XMP tags
if ExifTags.Base.Orientation not in self._exif:
xmp_tags = self.info.get("XML:com.adobe.xmp")
pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])'
if not xmp_tags and (xmp_tags := self.info.get("xmp")):
xmp_tags = xmp_tags.decode("utf-8")
pattern = rb'tiff:Orientation(="|>)([0-9])'
if xmp_tags:
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
match = re.search(pattern, xmp_tags)
if match:
self._exif[ExifTags.Base.Orientation] = int(match[2])
@ -3267,7 +3272,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
:param obj: Object with array interface
:param mode: Optional mode to use when reading ``obj``. Will be determined from
type if ``None``.
type if ``None``. Deprecated.
This will not be used to convert the data after reading, but will be used to
change how the data is read::
@ -3302,6 +3307,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
msg = f"Cannot handle this data type: {typekey_shape}, {typestr}"
raise TypeError(msg) from e
else:
deprecate("'mode' parameter", 13)
rawmode = mode
if mode in ["1", "L", "I", "P", "F"]:
ndmax = 2

View File

@ -248,6 +248,9 @@ class ImageCmsProfile:
low-level profile object
"""
self.filename = None
self.product_name = None # profile.product_name
self.product_info = None # profile.product_info
if isinstance(profile, str):
if sys.platform == "win32":
@ -256,23 +259,18 @@ class ImageCmsProfile:
profile_bytes_path.decode("ascii")
except UnicodeDecodeError:
with open(profile, "rb") as f:
self._set(core.profile_frombytes(f.read()))
self.profile = core.profile_frombytes(f.read())
return
self._set(core.profile_open(profile), profile)
self.filename = profile
self.profile = core.profile_open(profile)
elif hasattr(profile, "read"):
self._set(core.profile_frombytes(profile.read()))
self.profile = core.profile_frombytes(profile.read())
elif isinstance(profile, core.CmsProfile):
self._set(profile)
self.profile = profile
else:
msg = "Invalid type for Profile" # type: ignore[unreachable]
raise TypeError(msg)
def _set(self, profile: core.CmsProfile, filename: str | None = None) -> None:
self.profile = profile
self.filename = filename
self.product_name = None # profile.product_name
self.product_info = None # profile.product_info
def tobytes(self) -> bytes:
"""
Returns the profile in a format suitable for embedding in

View File

@ -365,22 +365,10 @@ class ImageDraw:
# use the fill as a mask
mask = Image.new("1", self.im.size)
mask_ink = self._getink(1)[0]
fill_im = mask.copy()
draw = Draw(fill_im)
draw = Draw(mask)
draw.draw.draw_polygon(xy, mask_ink, 1)
ink_im = mask.copy()
draw = Draw(ink_im)
width = width * 2 - 1
draw.draw.draw_polygon(xy, mask_ink, 0, width)
mask.paste(ink_im, mask=fill_im)
im = Image.new(self.mode, self.im.size)
draw = Draw(im)
draw.draw.draw_polygon(xy, ink, 0, width)
self.im.paste(im.im, (0, 0) + im.size, mask.im)
self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im)
def regular_polygon(
self,

View File

@ -257,6 +257,7 @@ class ImageFile(Image.Image):
def __setstate__(self, state: list[Any]) -> None:
self.tile = []
if len(state) > 5:
self.filename = state[5]
super().__setstate__(state)

View File

@ -134,10 +134,10 @@ def grabclipboard() -> Image.Image | list[str] | None:
import struct
o = struct.unpack_from("I", data)[0]
if data[16] != 0:
files = data[o:].decode("utf-16le").split("\0")
else:
if data[16] == 0:
files = data[o:].decode("mbcs").split("\0")
else:
files = data[o:].decode("utf-16le").split("\0")
return files[: files.index("")]
if isinstance(data, bytes):
data = io.BytesIO(data)

View File

@ -175,7 +175,9 @@ class MacViewer(Viewer):
if not os.path.exists(path):
raise FileNotFoundError
subprocess.call(["open", "-a", "Preview.app", path])
executable = sys.executable or shutil.which("python3")
pyinstaller = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")
executable = (not pyinstaller and sys.executable) or shutil.which("python3")
if executable:
subprocess.Popen(
[

View File

@ -762,8 +762,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
extra = info.get("extra", b"")
MAX_BYTES_IN_MARKER = 65533
xmp = info.get("xmp")
if xmp:
if xmp := info.get("xmp"):
overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
if len(xmp) > max_data_bytes_in_marker:
@ -772,8 +771,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
size = o16(2 + overhead_len + len(xmp))
extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp
icc_profile = info.get("icc_profile")
if icc_profile:
if icc_profile := info.get("icc_profile"):
overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers))
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
markers = []
@ -831,7 +829,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# in a shot. Guessing on the size, at im.size bytes. (raw pixel size is
# channels*size, this is a value that's been used in a django patch.
# https://github.com/matthewwithanm/django-imagekit/issues/50
bufsize = 0
if optimize or progressive:
# CMYK can be bigger
if im.mode == "CMYK":
@ -848,7 +845,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
else:
# The EXIF info needs to be written as one block, + APP1, + one spare byte.
# Ensure that our buffer is big enough. Same with the icc_profile block.
bufsize = max(bufsize, len(exif) + 5, len(extra) + 1)
bufsize = max(len(exif) + 5, len(extra) + 1)
ImageFile._save(
im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize

View File

@ -33,11 +33,7 @@ class BitStream:
def peek(self, bits: int) -> int:
while self.bits < bits:
c = self.next()
if c < 0:
self.bits = 0
continue
self.bitbuffer = (self.bitbuffer << 8) + c
self.bitbuffer = (self.bitbuffer << 8) + self.next()
self.bits += 8
return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1

View File

@ -54,7 +54,7 @@ class PcxImageFile(ImageFile.ImageFile):
# header
assert self.fp is not None
s = self.fp.read(128)
s = self.fp.read(68)
if not _accept(s):
msg = "not a PCX file"
raise SyntaxError(msg)
@ -66,6 +66,8 @@ class PcxImageFile(ImageFile.ImageFile):
raise SyntaxError(msg)
logger.debug("BBox: %s %s %s %s", *bbox)
offset = self.fp.tell() + 60
# format
version = s[1]
bits = s[3]
@ -102,7 +104,6 @@ class PcxImageFile(ImageFile.ImageFile):
break
if mode == "P":
self.palette = ImagePalette.raw("RGB", s[1:])
self.fp.seek(128)
elif version == 5 and bits == 8 and planes == 3:
mode = "RGB"
@ -128,9 +129,7 @@ class PcxImageFile(ImageFile.ImageFile):
bbox = (0, 0) + self.size
logger.debug("size: %sx%s", *self.size)
self.tile = [
ImageFile._Tile("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))
]
self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))]
# --------------------------------------------------------------------

View File

@ -48,6 +48,7 @@ from ._binary import i32be as i32
from ._binary import o8
from ._binary import o16be as o16
from ._binary import o32be as o32
from ._deprecate import deprecate
from ._util import DeferredError
TYPE_CHECKING = False
@ -1368,6 +1369,8 @@ def _save(
except KeyError as e:
msg = f"cannot write mode {mode} as PNG"
raise OSError(msg) from e
if outmode == "I":
deprecate("Saving I mode images as PNG", 13, stacklevel=4)
#
# write minimal PNG file

View File

@ -94,8 +94,8 @@ class PpmImageFile(ImageFile.ImageFile):
msg = "Reached EOF while reading header"
raise ValueError(msg)
elif len(token) > 10:
msg = f"Token too long in file header: {token.decode()}"
raise ValueError(msg)
msg_too_long = b"Token too long in file header: %s" % token
raise ValueError(msg_too_long)
return token
def _open(self) -> None:

View File

@ -8,9 +8,12 @@
from __future__ import annotations
import os
from typing import IO
from . import Image, ImageFile
from ._binary import i32be as i32
from ._binary import o8
from ._binary import o32be as o32
def _accept(prefix: bytes) -> bool:
@ -51,7 +54,7 @@ class QoiDecoder(ImageFile.PyDecoder):
assert self.fd is not None
self._previously_seen_pixels = {}
self._add_to_previous_pixels(bytearray((0, 0, 0, 255)))
self._previous_pixel = bytearray((0, 0, 0, 255))
data = bytearray()
bands = Image.getmodebands(self.mode)
@ -110,6 +113,122 @@ class QoiDecoder(ImageFile.PyDecoder):
return -1, 0
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode == "RGB":
channels = 3
elif im.mode == "RGBA":
channels = 4
else:
msg = "Unsupported QOI image mode"
raise ValueError(msg)
colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1
fp.write(b"qoif")
fp.write(o32(im.size[0]))
fp.write(o32(im.size[1]))
fp.write(o8(channels))
fp.write(o8(colorspace))
ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)])
class QoiEncoder(ImageFile.PyEncoder):
_pushes_fd = True
_previous_pixel: tuple[int, int, int, int] | None = None
_previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {}
_run = 0
def _write_run(self) -> bytes:
data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN
self._run = 0
return data
def _delta(self, left: int, right: int) -> int:
result = (left - right) & 255
if result >= 128:
result -= 256
return result
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
assert self.im is not None
self._previously_seen_pixels = {0: (0, 0, 0, 0)}
self._previous_pixel = (0, 0, 0, 255)
data = bytearray()
w, h = self.im.size
bands = Image.getmodebands(self.mode)
for y in range(h):
for x in range(w):
pixel = self.im.getpixel((x, y))
if bands == 3:
pixel = (*pixel, 255)
if pixel == self._previous_pixel:
self._run += 1
if self._run == 62:
data += self._write_run()
else:
if self._run:
data += self._write_run()
r, g, b, a = pixel
hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
if self._previously_seen_pixels.get(hash_value) == pixel:
data += o8(hash_value) # QOI_OP_INDEX
elif self._previous_pixel:
self._previously_seen_pixels[hash_value] = pixel
prev_r, prev_g, prev_b, prev_a = self._previous_pixel
if prev_a == a:
delta_r = self._delta(r, prev_r)
delta_g = self._delta(g, prev_g)
delta_b = self._delta(b, prev_b)
if (
-2 <= delta_r < 2
and -2 <= delta_g < 2
and -2 <= delta_b < 2
):
data += o8(
0b01000000
| (delta_r + 2) << 4
| (delta_g + 2) << 2
| (delta_b + 2)
) # QOI_OP_DIFF
else:
delta_gr = self._delta(delta_r, delta_g)
delta_gb = self._delta(delta_b, delta_g)
if (
-8 <= delta_gr < 8
and -32 <= delta_g < 32
and -8 <= delta_gb < 8
):
data += o8(
0b10000000 | (delta_g + 32)
) # QOI_OP_LUMA
data += o8((delta_gr + 8) << 4 | (delta_gb + 8))
else:
data += o8(0b11111110) # QOI_OP_RGB
data += bytes(pixel[:3])
else:
data += o8(0b11111111) # QOI_OP_RGBA
data += bytes(pixel)
self._previous_pixel = pixel
if self._run:
data += self._write_run()
data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding
return len(data), 0, data
Image.register_open(QoiImageFile.format, QoiImageFile, _accept)
Image.register_decoder("qoi", QoiDecoder)
Image.register_extension(QoiImageFile.format, ".qoi")
Image.register_save(QoiImageFile.format, _save)
Image.register_encoder("qoi", QoiEncoder)

View File

@ -1217,9 +1217,10 @@ class TiffImageFile(ImageFile.ImageFile):
return
self._seek(frame)
if self._im is not None and (
self.im.size != self._tile_size or self.im.mode != self.mode
self.im.size != self._tile_size
or self.im.mode != self.mode
or self.readonly
):
# The core image will no longer be used
self._im = None
def _seek(self, frame: int) -> None:
@ -1259,7 +1260,10 @@ class TiffImageFile(ImageFile.ImageFile):
self.fp.seek(self._frame_pos[frame])
self.tag_v2.load(self.fp)
if XMP in self.tag_v2:
self.info["xmp"] = self.tag_v2[XMP]
xmp = self.tag_v2[XMP]
if isinstance(xmp, tuple) and len(xmp) == 1:
xmp = xmp[0]
self.info["xmp"] = xmp
elif "xmp" in self.info:
del self.info["xmp"]
self._reload_exif()
@ -1676,7 +1680,7 @@ SAVE_INFO = {
"PA": ("PA", II, 3, 1, (8, 8), 2),
"I": ("I;32S", II, 1, 2, (32,), None),
"I;16": ("I;16", II, 1, 1, (16,), None),
"I;16S": ("I;16S", II, 1, 2, (16,), None),
"I;16L": ("I;16L", II, 1, 1, (16,), None),
"F": ("F;32F", II, 1, 3, (32,), None),
"RGB": ("RGB", II, 2, 1, (8, 8, 8), None),
"RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0),
@ -1684,10 +1688,7 @@ SAVE_INFO = {
"CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None),
"YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None),
"LAB": ("LAB", II, 8, 1, (8, 8, 8), None),
"I;32BS": ("I;32BS", MM, 1, 2, (32,), None),
"I;16B": ("I;16B", MM, 1, 1, (16,), None),
"I;16BS": ("I;16BS", MM, 1, 2, (16,), None),
"F;32BF": ("F;32BF", MM, 1, 3, (32,), None),
}
@ -1963,7 +1964,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# we're storing image byte order. So, if the rawmode
# contains I;16, we need to convert from native to image
# byte order.
if im.mode in ("I;16B", "I;16"):
if im.mode in ("I;16", "I;16B", "I;16L"):
rawmode = "I;16N"
# Pass tags as sorted list so that the tags are set in a fixed order.

View File

@ -81,7 +81,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
def _open(self) -> None:
# check placable header
s = self.fp.read(80)
s = self.fp.read(44)
if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"):
# placeable windows metafile

View File

@ -12,6 +12,7 @@ def deprecate(
*,
action: str | None = None,
plural: bool = False,
stacklevel: int = 3,
) -> None:
"""
Deprecations helper.
@ -67,5 +68,5 @@ def deprecate(
warnings.warn(
f"{deprecated} {is_} deprecated and will be removed in {removed}{action}",
DeprecationWarning,
stacklevel=3,
stacklevel=stacklevel,
)

View File

@ -881,26 +881,22 @@ setup_module(PyObject *m) {
return 0;
}
static PyModuleDef_Slot slots[] = {
{Py_mod_exec, setup_module},
#ifdef Py_GIL_DISABLED
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
PyMODINIT_FUNC
PyInit__avif(void) {
PyObject *m;
static PyModuleDef module_def = {
PyModuleDef_HEAD_INIT,
.m_name = "_avif",
.m_size = -1,
.m_methods = avifMethods,
.m_slots = slots
};
m = PyModule_Create(&module_def);
if (setup_module(m) < 0) {
Py_DECREF(m);
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
return PyModuleDef_Init(&module_def);
}

View File

@ -308,8 +308,8 @@ _new_arrow(PyObject *self, PyObject *args) {
}
// ImagingBorrowArrow is responsible for retaining the array_capsule
ret =
PyImagingNew(ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule)
ret = PyImagingNew(
ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule)
);
if (!ret) {
return ImagingError_ValueError("Invalid Arrow array mode or size mismatch");
@ -338,12 +338,6 @@ static const char *no_palette = "image has no palette";
static const char *readonly = "image is readonly";
/* static const char* no_content = "image has no content"; */
void *
ImagingError_OSError(void) {
PyErr_SetString(PyExc_OSError, "error when accessing file");
return NULL;
}
void *
ImagingError_MemoryError(void) {
return PyErr_NoMemory();
@ -369,11 +363,6 @@ ImagingError_ValueError(const char *message) {
return NULL;
}
void
ImagingError_Clear(void) {
PyErr_Clear();
}
/* -------------------------------------------------------------------- */
/* HELPERS */
/* -------------------------------------------------------------------- */
@ -1665,7 +1654,8 @@ _putdata(ImagingObject *self, PyObject *args) {
int bigendian = 0;
if (image->type == IMAGING_TYPE_SPECIAL) {
// I;16*
if (strcmp(image->mode, "I;16B") == 0
if (
strcmp(image->mode, "I;16B") == 0
#ifdef WORDS_BIGENDIAN
|| strcmp(image->mode, "I;16N") == 0
#endif
@ -2226,6 +2216,7 @@ _unsharp_mask(ImagingObject *self, PyObject *args) {
}
if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) {
ImagingDelete(imOut);
return NULL;
}
@ -3218,7 +3209,8 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) {
(int)p[3],
&ink,
width,
self->blend
self->blend,
NULL
) < 0) {
free(xy);
return NULL;
@ -3356,7 +3348,10 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) {
int ink;
int fill = 0;
int width = 0;
if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) {
ImagingObject *maskp = NULL;
if (!PyArg_ParseTuple(
args, "Oi|iiO!", &data, &ink, &fill, &width, &Imaging_Type, &maskp
)) {
return NULL;
}
@ -3386,8 +3381,16 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) {
free(xy);
if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) <
0) {
if (ImagingDrawPolygon(
self->image->image,
n,
ixy,
&ink,
fill,
width,
self->blend,
maskp ? maskp->image : NULL
) < 0) {
free(ixy);
return NULL;
}
@ -4460,27 +4463,22 @@ setup_module(PyObject *m) {
return 0;
}
static PyModuleDef_Slot slots[] = {
{Py_mod_exec, setup_module},
#ifdef Py_GIL_DISABLED
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
PyMODINIT_FUNC
PyInit__imaging(void) {
PyObject *m;
static PyModuleDef module_def = {
PyModuleDef_HEAD_INIT,
.m_name = "_imaging",
.m_size = -1,
.m_methods = functions,
.m_slots = slots
};
m = PyModule_Create(&module_def);
if (setup_module(m) < 0) {
Py_DECREF(m);
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
return PyModuleDef_Init(&module_def);
}

View File

@ -1463,28 +1463,24 @@ setup_module(PyObject *m) {
return 0;
}
static PyModuleDef_Slot slots[] = {
{Py_mod_exec, setup_module},
#ifdef Py_GIL_DISABLED
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
PyMODINIT_FUNC
PyInit__imagingcms(void) {
PyObject *m;
PyDateTime_IMPORT;
static PyModuleDef module_def = {
PyModuleDef_HEAD_INIT,
.m_name = "_imagingcms",
.m_size = -1,
.m_methods = pyCMSdll_methods,
.m_slots = slots
};
m = PyModule_Create(&module_def);
if (setup_module(m) < 0) {
return NULL;
}
PyDateTime_IMPORT;
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
return PyModuleDef_Init(&module_def);
}

View File

@ -275,6 +275,7 @@ text_layout_raqm(
if (!text || !size) {
/* return 0 and clean up, no glyphs==no size,
and raqm fails with empty strings */
PyMem_Free(text);
goto failed;
}
set_text = raqm_set_text(rq, text, size);
@ -425,6 +426,7 @@ text_layout_fallback(
"setting text direction, language or font features is not supported "
"without libraqm"
);
return 0;
}
if (PyUnicode_Check(string)) {
@ -1599,26 +1601,22 @@ setup_module(PyObject *m) {
return 0;
}
static PyModuleDef_Slot slots[] = {
{Py_mod_exec, setup_module},
#ifdef Py_GIL_DISABLED
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
PyMODINIT_FUNC
PyInit__imagingft(void) {
PyObject *m;
static PyModuleDef module_def = {
PyModuleDef_HEAD_INIT,
.m_name = "_imagingft",
.m_size = -1,
.m_methods = _functions,
.m_slots = slots
};
m = PyModule_Create(&module_def);
if (setup_module(m) < 0) {
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
return PyModuleDef_Init(&module_def);
}

View File

@ -302,26 +302,22 @@ setup_module(PyObject *m) {
return 0;
}
static PyModuleDef_Slot slots[] = {
{Py_mod_exec, setup_module},
#ifdef Py_GIL_DISABLED
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
PyMODINIT_FUNC
PyInit__imagingmath(void) {
PyObject *m;
static PyModuleDef module_def = {
PyModuleDef_HEAD_INIT,
.m_name = "_imagingmath",
.m_size = -1,
.m_methods = _functions,
.m_slots = slots
};
m = PyModule_Create(&module_def);
if (setup_module(m) < 0) {
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
return PyModuleDef_Init(&module_def);
}

View File

@ -246,23 +246,22 @@ static PyMethodDef functions[] = {
{NULL, NULL, 0, NULL}
};
static PyModuleDef_Slot slots[] = {
#ifdef Py_GIL_DISABLED
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
PyMODINIT_FUNC
PyInit__imagingmorph(void) {
PyObject *m;
static PyModuleDef module_def = {
PyModuleDef_HEAD_INIT,
.m_name = "_imagingmorph",
.m_doc = "A module for doing image morphology",
.m_size = -1,
.m_methods = functions,
.m_slots = slots
};
m = PyModule_Create(&module_def);
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
return PyModuleDef_Init(&module_def);
}

View File

@ -46,24 +46,22 @@ static PyMethodDef functions[] = {
{NULL, NULL} /* sentinel */
};
static PyModuleDef_Slot slots[] = {
{Py_mod_exec, load_tkinter_funcs},
#ifdef Py_GIL_DISABLED
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
PyMODINIT_FUNC
PyInit__imagingtk(void) {
static PyModuleDef module_def = {
PyModuleDef_HEAD_INIT,
.m_name = "_imagingtk",
.m_size = -1,
.m_methods = functions,
.m_slots = slots
};
PyObject *m;
m = PyModule_Create(&module_def);
if (load_tkinter_funcs() != 0) {
Py_DECREF(m);
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
return PyModuleDef_Init(&module_def);
}

View File

@ -641,6 +641,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
ImagingSectionLeave(&cookie);
WebPPictureFree(&pic);
output = writer.mem;
ret_size = writer.size;
if (!ok) {
int error_code = (&pic)->error_code;
char message[50] = "";
@ -652,10 +656,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
);
}
PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message);
free(output);
return NULL;
}
output = writer.mem;
ret_size = writer.size;
{
/* I want to truncate the *_size items that get passed into WebP
@ -777,26 +780,22 @@ setup_module(PyObject *m) {
return 0;
}
static PyModuleDef_Slot slots[] = {
{Py_mod_exec, setup_module},
#ifdef Py_GIL_DISABLED
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
PyMODINIT_FUNC
PyInit__webp(void) {
PyObject *m;
static PyModuleDef module_def = {
PyModuleDef_HEAD_INIT,
.m_name = "_webp",
.m_size = -1,
.m_methods = webpMethods,
.m_slots = slots
};
m = PyModule_Create(&module_def);
if (setup_module(m) < 0) {
Py_DECREF(m);
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
return PyModuleDef_Init(&module_def);
}

View File

@ -327,11 +327,11 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
// added in Windows 10 (1607)
// loaded dynamically to avoid link errors
user32 = LoadLibraryA("User32.dll");
SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext
)GetProcAddress(user32, "SetThreadDpiAwarenessContext");
SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext)
GetProcAddress(user32, "SetThreadDpiAwarenessContext");
if (SetThreadDpiAwarenessContext_function != NULL) {
GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext
)GetProcAddress(user32, "GetWindowDpiAwarenessContext");
GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext)
GetProcAddress(user32, "GetWindowDpiAwarenessContext");
if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) {
dpiAwareness = GetWindowDpiAwarenessContext_function(wnd);
}

View File

@ -703,6 +703,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
return NULL;
}
encoder->cleanup = ImagingLibTiffEncodeCleanup;
num_core_tags = sizeof(core_tags) / sizeof(int);
for (pos = 0; pos < tags_size; pos++) {
item = PyList_GetItemRef(tags, pos);

View File

@ -36,7 +36,10 @@ ReleaseExportedSchema(struct ArrowSchema *array) {
child->release(child);
child->release = NULL;
}
// UNDONE -- should I be releasing the children?
free(array->children[i]);
}
if (array->children) {
free(array->children);
}
// Release dictionary
@ -98,7 +101,7 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) {
}
/* for now, single block images */
if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
if (im->blocks_count > 1) {
return IMAGING_ARROW_MEMORY_LAYOUT;
}
@ -117,6 +120,7 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) {
retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel");
if (retval != 0) {
free(schema->children[0]);
free(schema->children);
schema->release(schema);
return retval;
}
@ -127,9 +131,7 @@ static void
release_const_array(struct ArrowArray *array) {
Imaging im = (Imaging)array->private_data;
if (array->n_children == 0) {
ImagingDelete(im);
}
// Free the buffers and the buffers array
if (array->buffers) {
@ -157,7 +159,7 @@ export_single_channel_array(Imaging im, struct ArrowArray *array) {
int length = im->xsize * im->ysize;
/* for now, single block images */
if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
if (im->blocks_count > 1) {
return IMAGING_ARROW_MEMORY_LAYOUT;
}
@ -200,7 +202,7 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) {
int length = im->xsize * im->ysize;
/* for now, single block images */
if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
if (im->blocks_count > 1) {
return IMAGING_ARROW_MEMORY_LAYOUT;
}

View File

@ -63,7 +63,7 @@ typedef struct {
} Edge;
/* Type used in "polygon*" functions */
typedef void (*hline_handler)(Imaging, int, int, int, int);
typedef void (*hline_handler)(Imaging, int, int, int, int, Imaging);
static inline void
point8(Imaging im, int x, int y, int ink) {
@ -103,9 +103,7 @@ point32rgba(Imaging im, int x, int y, int ink) {
}
static inline void
hline8(Imaging im, int x0, int y0, int x1, int ink) {
int pixelwidth;
hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) {
if (y0 >= 0 && y0 < im->ysize) {
if (x0 < 0) {
x0 = 0;
@ -118,16 +116,41 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) {
x1 = im->xsize - 1;
}
if (x0 <= x1) {
pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1;
memset(
im->image8[y0] + x0 * pixelwidth, (UINT8)ink, (x1 - x0 + 1) * pixelwidth
);
int bigendian = -1;
if (strncmp(im->mode, "I;16", 4) == 0) {
bigendian =
(
#ifdef WORDS_BIGENDIAN
strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16L") == 0
#else
strcmp(im->mode, "I;16B") == 0
#endif
)
? 1
: 0;
}
if (mask == NULL && bigendian == -1) {
memset(im->image8[y0] + x0, (UINT8)ink, (x1 - x0 + 1));
} else {
UINT8 *p = im->image8[y0];
while (x0 <= x1) {
if (mask == NULL || mask->image8[y0][x0]) {
if (bigendian == -1) {
p[x0] = ink;
} else {
p[x0 * 2 + (bigendian ? 1 : 0)] = ink;
p[x0 * 2 + (bigendian ? 0 : 1)] = ink >> 8;
}
}
x0++;
}
}
}
}
}
static inline void
hline32(Imaging im, int x0, int y0, int x1, int ink) {
hline32(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) {
INT32 *p;
if (y0 >= 0 && y0 < im->ysize) {
@ -143,13 +166,16 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) {
}
p = im->image32[y0];
while (x0 <= x1) {
p[x0++] = ink;
if (mask == NULL || mask->image8[y0][x0]) {
p[x0] = ink;
}
x0++;
}
}
}
static inline void
hline32rgba(Imaging im, int x0, int y0, int x1, int ink) {
hline32rgba(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) {
unsigned int tmp;
if (y0 >= 0 && y0 < im->ysize) {
@ -167,9 +193,11 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink) {
UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4;
UINT8 *in = (UINT8 *)&ink;
while (x0 <= x1) {
if (mask == NULL || mask->image8[y0][x0]) {
out[0] = BLEND(in[3], out[0], in[0], tmp);
out[1] = BLEND(in[3], out[1], in[1], tmp);
out[2] = BLEND(in[3], out[2], in[2], tmp);
}
x0++;
out += 4;
}
@ -407,7 +435,14 @@ x_cmp(const void *x0, const void *x1) {
static void
draw_horizontal_lines(
Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline
Imaging im,
int n,
Edge *e,
int ink,
int *x_pos,
int y,
hline_handler hline,
Imaging mask
) {
int i;
for (i = 0; i < n; i++) {
@ -429,7 +464,7 @@ draw_horizontal_lines(
}
}
(*hline)(im, xmin, e[i].ymin, xmax, ink);
(*hline)(im, xmin, e[i].ymin, xmax, ink, mask);
*x_pos = xmax + 1;
}
}
@ -440,7 +475,7 @@ draw_horizontal_lines(
*/
static inline int
polygon_generic(
Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, int hasAlpha
Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, Imaging mask
) {
Edge **edge_table;
float *xx;
@ -461,6 +496,7 @@ polygon_generic(
return -1;
}
int hasAlpha = hline == hline32rgba;
for (i = 0; i < n; i++) {
if (ymin > e[i].ymin) {
ymin = e[i].ymin;
@ -470,7 +506,7 @@ polygon_generic(
}
if (e[i].ymin == e[i].ymax) {
if (hasAlpha != 1) {
(*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink);
(*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink, mask);
}
continue;
}
@ -558,7 +594,7 @@ polygon_generic(
// Line would be before the current position
continue;
}
draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline);
draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask);
if (x_end < x_pos) {
// Line would be before the current position
continue;
@ -574,13 +610,13 @@ polygon_generic(
continue;
}
}
(*hline)(im, x_start, ymin, x_end, ink);
(*hline)(im, x_start, ymin, x_end, ink, mask);
x_pos = x_end + 1;
}
draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline);
draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask);
} else {
for (i = 1; i < j; i += 2) {
(*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink);
(*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink, mask);
}
}
}
@ -590,21 +626,6 @@ polygon_generic(
return 0;
}
static inline int
polygon8(Imaging im, int n, Edge *e, int ink, int eofill) {
return polygon_generic(im, n, e, ink, eofill, hline8, 0);
}
static inline int
polygon32(Imaging im, int n, Edge *e, int ink, int eofill) {
return polygon_generic(im, n, e, ink, eofill, hline32, 0);
}
static inline int
polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill) {
return polygon_generic(im, n, e, ink, eofill, hline32rgba, 1);
}
static inline void
add_edge(Edge *e, int x0, int y0, int x1, int y1) {
/* printf("edge %d %d %d %d\n", x0, y0, x1, y1); */
@ -639,14 +660,13 @@ add_edge(Edge *e, int x0, int y0, int x1, int y1) {
typedef struct {
void (*point)(Imaging im, int x, int y, int ink);
void (*hline)(Imaging im, int x0, int y0, int x1, int ink);
void (*hline)(Imaging im, int x0, int y0, int x1, int ink, Imaging mask);
void (*line)(Imaging im, int x0, int y0, int x1, int y1, int ink);
int (*polygon)(Imaging im, int n, Edge *e, int ink, int eofill);
} DRAW;
DRAW draw8 = {point8, hline8, line8, polygon8};
DRAW draw32 = {point32, hline32, line32, polygon32};
DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba, polygon32rgba};
DRAW draw8 = {point8, hline8, line8};
DRAW draw32 = {point32, hline32, line32};
DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba};
/* -------------------------------------------------------------------- */
/* Interface */
@ -691,7 +711,15 @@ ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink_, in
int
ImagingDrawWideLine(
Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int width, int op
Imaging im,
int x0,
int y0,
int x1,
int y1,
const void *ink_,
int width,
int op,
Imaging mask
) {
DRAW *draw;
INT32 ink;
@ -731,7 +759,7 @@ ImagingDrawWideLine(
add_edge(e + 2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]);
add_edge(e + 3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]);
draw->polygon(im, 4, e, ink, 0);
polygon_generic(im, 4, e, ink, 0, draw->hline, mask);
}
return 0;
}
@ -774,7 +802,7 @@ ImagingDrawRectangle(
}
for (y = y0; y <= y1; y++) {
draw->hline(im, x0, y, x1, ink);
draw->hline(im, x0, y, x1, ink, NULL);
}
} else {
@ -783,8 +811,8 @@ ImagingDrawRectangle(
width = 1;
}
for (i = 0; i < width; i++) {
draw->hline(im, x0, y0 + i, x1, ink);
draw->hline(im, x0, y1 - i, x1, ink);
draw->hline(im, x0, y0 + i, x1, ink, NULL);
draw->hline(im, x0, y1 - i, x1, ink, NULL);
draw->line(im, x1 - i, y0 + width, x1 - i, y1 - width + 1, ink);
draw->line(im, x0 + i, y0 + width, x0 + i, y1 - width + 1, ink);
}
@ -795,7 +823,14 @@ ImagingDrawRectangle(
int
ImagingDrawPolygon(
Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op
Imaging im,
int count,
int *xy,
const void *ink_,
int fill,
int width,
int op,
Imaging mask
) {
int i, n, x0, y0, x1, y1;
DRAW *draw;
@ -839,7 +874,7 @@ ImagingDrawPolygon(
if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) {
add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]);
}
draw->polygon(im, n, e, ink, 0);
polygon_generic(im, n, e, ink, 0, draw->hline, mask);
free(e);
} else {
@ -861,11 +896,12 @@ ImagingDrawPolygon(
xy[i * 2 + 3],
ink_,
width,
op
op,
mask
);
}
ImagingDrawWideLine(
im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op
im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op, mask
);
}
}
@ -1536,7 +1572,9 @@ ellipseNew(
ellipse_init(&st, a, b, width);
int32_t X0, Y, X1;
while (ellipse_next(&st, &X0, &Y, &X1) != -1) {
draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink);
draw->hline(
im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL
);
}
return 0;
}
@ -1571,7 +1609,9 @@ clipEllipseNew(
int32_t X0, Y, X1;
int next_code;
while ((next_code = clip_ellipse_next(&st, &X0, &Y, &X1)) >= 0) {
draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink);
draw->hline(
im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL
);
}
clip_ellipse_free(&st);
return next_code == -1 ? 0 : -1;
@ -1989,7 +2029,7 @@ ImagingDrawOutline(
DRAWINIT();
draw->polygon(im, outline->count, outline->edges, ink, 0);
polygon_generic(im, outline->count, outline->edges, ink, 0, draw->hline, NULL);
return 0;
}

View File

@ -54,7 +54,7 @@ ImagingSavePPM(Imaging im, const char *outfile) {
fp = fopen(outfile, "wb");
if (!fp) {
(void)ImagingError_OSError();
PyErr_SetString(PyExc_OSError, "error when accessing file");
return 0;
}

View File

@ -118,8 +118,9 @@ ImagingFillRadialGradient(const char *mode) {
for (y = 0; y < 256; y++) {
for (x = 0; x < 256; x++) {
d = (int
)sqrt((double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0);
d = (int)sqrt(
(double)((x - 128) * (x - 128) + (y - 128) * (y - 128)) * 2.0
);
if (d >= 255) {
d = 255;
}

View File

@ -155,7 +155,8 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
} else {
int bigendian = 0;
if (im->type == IMAGING_TYPE_SPECIAL) {
if (strcmp(im->mode, "I;16B") == 0
if (
strcmp(im->mode, "I;16B") == 0
#ifdef WORDS_BIGENDIAN
|| strcmp(im->mode, "I;16N") == 0
#endif
@ -308,7 +309,8 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) {
} else {
int bigendian = 0;
if (im->type == IMAGING_TYPE_SPECIAL) {
if (strcmp(im->mode, "I;16B") == 0
if (
strcmp(im->mode, "I;16B") == 0
#ifdef WORDS_BIGENDIAN
|| strcmp(im->mode, "I;16N") == 0
#endif

View File

@ -270,8 +270,6 @@ ImagingSectionLeave(ImagingSectionCookie *cookie);
/* Exceptions */
/* ---------- */
extern void *
ImagingError_OSError(void);
extern void *
ImagingError_MemoryError(void);
extern void *
@ -280,8 +278,6 @@ extern void *
ImagingError_Mismatch(void); /* maps to ValueError by default */
extern void *
ImagingError_ValueError(const char *message);
extern void
ImagingError_Clear(void);
/* Transform callbacks */
/* ------------------- */
@ -510,7 +506,15 @@ extern int
ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink, int op);
extern int
ImagingDrawWideLine(
Imaging im, int x0, int y0, int x1, int y1, const void *ink, int width, int op
Imaging im,
int x0,
int y0,
int x1,
int y1,
const void *ink,
int width,
int op,
Imaging mask
);
extern int
ImagingDrawPieslice(
@ -530,7 +534,14 @@ extern int
ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op);
extern int
ImagingDrawPolygon(
Imaging im, int points, int *xy, const void *ink, int fill, int width, int op
Imaging im,
int points,
int *xy,
const void *ink,
int fill,
int width,
int op,
Imaging mask
);
extern int
ImagingDrawRectangle(

View File

@ -207,8 +207,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) {
if (params->cp_cinema == OPJ_CINEMA4K_24) {
float max_rate =
((float)(components * im->xsize * im->ysize * 8) / (CINEMA_24_CS_LENGTH * 8)
);
((float)(components * im->xsize * im->ysize * 8) /
(CINEMA_24_CS_LENGTH * 8));
params->POC[0].tile = 1;
params->POC[0].resno0 = 0;
@ -243,8 +243,8 @@ j2k_set_cinema_params(Imaging im, int components, opj_cparameters_t *params) {
params->max_comp_size = COMP_24_CS_MAX_LENGTH;
} else {
float max_rate =
((float)(components * im->xsize * im->ysize * 8) / (CINEMA_48_CS_LENGTH * 8)
);
((float)(components * im->xsize * im->ysize * 8) /
(CINEMA_48_CS_LENGTH * 8));
for (n = 0; n < params->tcp_numlayers; ++n) {
rate = 0;

View File

@ -131,6 +131,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
break;
default:
state->errcode = IMAGING_CODEC_CONFIG;
jpeg_destroy_compress(&context->cinfo);
return -1;
}
@ -161,6 +162,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
/* Would subsample the green and blue
channels, which doesn't make sense */
state->errcode = IMAGING_CODEC_CONFIG;
jpeg_destroy_compress(&context->cinfo);
return -1;
}
jpeg_set_colorspace(&context->cinfo, JCS_RGB);

View File

@ -60,15 +60,25 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
}
if (state->x >= state->bytes) {
if (state->bytes % state->xsize && state->bytes > state->xsize) {
int bands = state->bytes / state->xsize;
int stride = state->bytes / bands;
int bands;
int xsize = 0;
int stride = 0;
if (state->bits == 2 || state->bits == 4) {
xsize = (state->xsize + 7) / 8;
bands = state->bits;
stride = state->bytes / state->bits;
} else {
xsize = state->xsize;
bands = state->bytes / state->xsize;
if (bands != 0) {
stride = state->bytes / bands;
}
}
if (stride > xsize) {
int i;
for (i = 1; i < bands; i++) { // note -- skipping first band
memmove(
&state->buffer[i * state->xsize],
&state->buffer[i * stride],
state->xsize
&state->buffer[i * xsize], &state->buffer[i * stride], xsize
);
}
}

View File

@ -197,8 +197,9 @@ ImagingPoint(Imaging imIn, const char *mode, const void *table) {
return imOut;
mode_mismatch:
return (Imaging
)ImagingError_ValueError("point operation not supported for this mode");
return (Imaging)ImagingError_ValueError(
"point operation not supported for this mode"
);
}
Imaging

View File

@ -470,7 +470,8 @@ ImagingResampleHorizontal_16bpc(
double *k;
int bigendian = 0;
if (strcmp(imIn->mode, "I;16N") == 0
if (
strcmp(imIn->mode, "I;16N") == 0
#ifdef WORDS_BIGENDIAN
|| strcmp(imIn->mode, "I;16B") == 0
#endif
@ -509,7 +510,8 @@ ImagingResampleVertical_16bpc(
double *k;
int bigendian = 0;
if (strcmp(imIn->mode, "I;16N") == 0
if (
strcmp(imIn->mode, "I;16N") == 0
#ifdef WORDS_BIGENDIAN
|| strcmp(imIn->mode, "I;16B") == 0
#endif

View File

@ -602,8 +602,9 @@ ImagingBorrowArrow(
}
if (!borrowed_buffer) {
return (Imaging
)ImagingError_ValueError("Arrow Array, exactly 2 buffers required");
return (Imaging)ImagingError_ValueError(
"Arrow Array, exactly 2 buffers required"
);
}
for (y = i = 0; y < im->ysize; y++) {
@ -644,7 +645,7 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) {
return im;
}
ImagingError_Clear();
PyErr_Clear();
// Try to allocate the image once more with smallest possible block size
MUTEX_LOCK(&ImagingDefaultArena.mutex);
@ -723,6 +724,8 @@ ImagingNewArrow(
int64_t pixels = (int64_t)xsize * (int64_t)ysize;
// fmt:off // don't reformat this
// stored as a single array, one element per pixel, either single band
// or multiband, where each pixel is an I32.
if (((strcmp(schema->format, "I") == 0 // int32
&& im->pixelsize == 4 // 4xchar* storage
&& im->bands >= 2) // INT32 into any INT32 Storage mode
@ -735,6 +738,7 @@ ImagingNewArrow(
return im;
}
}
// Stored as [[r,g,b,a],...]
if (strcmp(schema->format, "+w:4") == 0 // 4 up array
&& im->pixelsize == 4 // storage as 32 bpc
&& schema->n_children > 0 // make sure schema is well formed.
@ -750,6 +754,17 @@ ImagingNewArrow(
return im;
}
}
// Stored as [r,g,b,a,r,g,b,a,...]
if (strcmp(schema->format, "C") == 0 // uint8
&& im->pixelsize == 4 // storage as 32 bpc
&& schema->n_children == 0 // make sure schema is well formed.
&& strcmp(im->arrow_band_format, "C") == 0 // expected format
&& 4 * pixels == external_array->length) { // expected length
// single flat array, interleaved storage.
if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) {
return im;
}
}
// fmt: on
ImagingDelete(im);
return NULL;

View File

@ -557,7 +557,8 @@ _decodeStrip(
(tdata_t)state->buffer,
strip_size
) == -1) {
TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0))
TRACE(
("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0))
);
state->errcode = IMAGING_CODEC_BROKEN;
return -1;
@ -929,6 +930,27 @@ ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) {
return status;
}
int
ImagingLibTiffEncodeCleanup(ImagingCodecState state) {
TIFFSTATE *clientstate = (TIFFSTATE *)state->context;
TIFF *tiff = clientstate->tiff;
if (!tiff) {
return 0;
}
// TIFFClose in libtiff calls tif_closeproc and TIFFCleanup
if (clientstate->fp) {
// Python will manage the closing of the file rather than libtiff
// So only call TIFFCleanup
TIFFCleanup(tiff);
} else {
// When tif_closeproc refers to our custom _tiffCloseProc though,
// that is fine, as it does not close the file
TIFFClose(tiff);
}
return 0;
}
int
ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) {
/* One shot encoder. Encode everything to the tiff in the clientstate.
@ -1010,17 +1032,10 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
TRACE(("Encode Error, row %d\n", state->y));
state->errcode = IMAGING_CODEC_BROKEN;
// TIFFClose in libtiff calls tif_closeproc and TIFFCleanup
if (clientstate->fp) {
// Python will manage the closing of the file rather than libtiff
// So only call TIFFCleanup
TIFFCleanup(tiff);
clientstate->tiff = NULL;
} else {
// When tif_closeproc refers to our custom _tiffCloseProc though,
// that is fine, as it does not close the file
TIFFClose(tiff);
}
if (!clientstate->fp) {
free(clientstate->data);
}
return -1;
@ -1036,22 +1051,11 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
TRACE(("Error flushing the tiff"));
// likely reason is memory.
state->errcode = IMAGING_CODEC_MEMORY;
if (clientstate->fp) {
TIFFCleanup(tiff);
} else {
TIFFClose(tiff);
}
if (!clientstate->fp) {
free(clientstate->data);
}
return -1;
}
TRACE(("Closing \n"));
if (clientstate->fp) {
TIFFCleanup(tiff);
} else {
TIFFClose(tiff);
}
// reset the clientstate metadata to use it to read out the buffer.
clientstate->loc = 0;
clientstate->size = clientstate->eof; // redundant?

View File

@ -40,6 +40,8 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset);
extern int
ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp);
extern int
ImagingLibTiffEncodeCleanup(ImagingCodecState state);
extern int
ImagingLibTiffMergeFieldInfo(
ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length
);

View File

@ -137,6 +137,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) {
}
}
im->read_only = view.readonly;
im->destroy = mapping_destroy_buffer;
Py_INCREF(target);

View File

@ -3,7 +3,7 @@ requires =
tox>=4.2
env_list =
lint
py{py3, 313, 312, 311, 310, 39}
py{py3, 314, 313, 312, 311, 310, 39}
[testenv]
deps =

View File

@ -11,8 +11,7 @@ For more extensive info, see the [Windows build instructions](build.rst).
* Requires Microsoft Visual Studio 2017 or newer with C++ component.
* Requires NASM for libjpeg-turbo, a required dependency when using this script.
* Requires CMake 3.15 or newer (available as Visual Studio component).
* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise and Windows Server
2019 with Visual Studio 2019 Enterprise (GitHub Actions).
* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions).
Here's an example script to build on Windows:

View File

@ -113,12 +113,12 @@ V = {
"BROTLI": "1.1.0",
"FREETYPE": "2.13.3",
"FRIBIDI": "1.0.16",
"HARFBUZZ": "11.1.0",
"JPEGTURBO": "3.1.0",
"HARFBUZZ": "11.2.1",
"JPEGTURBO": "3.1.1",
"LCMS2": "2.17",
"LIBAVIF": "1.2.1",
"LIBAVIF": "1.3.0",
"LIBIMAGEQUANT": "4.3.4",
"LIBPNG": "1.6.47",
"LIBPNG": "1.6.49",
"LIBWEBP": "1.5.0",
"OPENJPEG": "2.5.3",
"TIFF": "4.7.0",
@ -385,10 +385,11 @@ DEPS: dict[str, dict[str, Any]] = {
"bins": [r"*.dll"],
},
"libavif": {
"url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip",
"filename": f"libavif-{V['LIBAVIF']}.zip",
"url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.tar.gz",
"filename": f"libavif-{V['LIBAVIF']}.tar.gz",
"license": "LICENSE",
"build": [
"rustup update",
f"{sys.executable} -m pip install meson",
*cmds_cmake(
"avif_static",
@ -399,7 +400,6 @@ DEPS: dict[str, dict[str, Any]] = {
"-DAVIF_CODEC_DAV1D=LOCAL",
"-DAVIF_CODEC_RAV1E=LOCAL",
"-DAVIF_CODEC_SVT=LOCAL",
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
),
cmd_xcopy("include", "{inc_dir}"),
],