Merge branch 'main' into chore/remove-rav1e

This commit is contained in:
Andrew Murray 2025-06-14 17:24:36 +10:00 committed by GitHub
commit 3ffce334b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 745 additions and 262 deletions

View File

@ -1 +1 @@
cibuildwheel==2.23.3 cibuildwheel==3.0.0

View File

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

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: jobs:
build: build:
runs-on: ${{ matrix.os }} runs-on: windows-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["pypy3.11", "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"] architecture: ["x64"]
os: ["windows-latest"]
include: include:
# Test the oldest Python on 32-bit # 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 timeout-minutes: 45

View File

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

View File

@ -40,7 +40,7 @@ ARCHIVE_SDIR=pillow-depends-main
FREETYPE_VERSION=2.13.3 FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=11.2.1 HARFBUZZ_VERSION=11.2.1
LIBPNG_VERSION=1.6.48 LIBPNG_VERSION=1.6.48
JPEGTURBO_VERSION=3.1.0 JPEGTURBO_VERSION=3.1.1
OPENJPEG_VERSION=2.5.3 OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.8.1 XZ_VERSION=5.8.1
TIFF_VERSION=4.7.0 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 C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null
} }
$env:path += ";$pillow\winbuild\build\bin\" $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 & 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\*") { if ("$venv" -like "*\cibw-run-*-win_amd64\*") {
& python -m pip install numpy & $venv\Scripts\$python -m pip install numpy
} }
cd $pillow cd $pillow
& python -VV & $venv\Scripts\$python -VV
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }
& python selftest.py & $venv\Scripts\$python selftest.py
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }
& python -m pytest -vx Tests\check_wheel.py & $venv\Scripts\$python -m pytest -vx Tests\check_wheel.py
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }
& python -m pytest -vx Tests & $venv\Scripts\$python -m pytest -vx Tests
if (!$?) { exit $LASTEXITCODE } if (!$?) { exit $LASTEXITCODE }

View File

@ -110,7 +110,6 @@ jobs:
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_SKIP: pp39-*
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
@ -188,7 +187,6 @@ jobs:
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw" CIBW_CACHE_PATH: "C:\\cibw"
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
CIBW_SKIP: pp39-*
CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow -v {project}:C:\pillow

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8 rev: v0.11.12
hooks: hooks:
- id: ruff - id: ruff
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v20.1.3 rev: v20.1.5
hooks: hooks:
- id: clang-format - id: clang-format
types: [c] types: [c]
@ -58,7 +58,7 @@ repos:
- id: check-renovate - id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit - repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.6.0 rev: v1.9.0
hooks: hooks:
- id: zizmor - id: zizmor
@ -68,7 +68,7 @@ repos:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.5.1 rev: v2.6.0
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt

View File

@ -97,13 +97,27 @@ test:
python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
python3 -m pytest -qq python3 -m pytest -qq
.PHONY: test-p
test-p:
python3 -c "import xdist" > /dev/null 2>&1 || python3 -m pip install pytest-xdist
python3 -m pytest -qq -n auto
.PHONY: valgrind .PHONY: valgrind
valgrind: valgrind:
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
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 \ --log-file=/tmp/valgrind-output \
python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output
.PHONY: valgrind-leak
valgrind-leak:
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \
--leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \
--log-file=/tmp/valgrind-output \
python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output
.PHONY: readme .PHONY: readme
readme: readme:
python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2

View File

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

View File

@ -161,6 +161,12 @@ def assert_tuple_approx_equal(
pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets))
def timeout_unless_slower_valgrind(timeout: float) -> pytest.MarkDecorator:
if "PILLOW_VALGRIND_TEST" in os.environ:
return pytest.mark.pil_noop_mark()
return pytest.mark.timeout(timeout)
def skip_unless_feature(feature: str) -> pytest.MarkDecorator: def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
reason = f"{feature} not available" reason = f"{feature} not available"
return pytest.mark.skipif(not features.check(feature), reason=reason) return pytest.mark.skipif(not features.check(feature), reason=reason)

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 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: def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
expected = r""
with pytest.raises(RuntimeError, match=expected): with pytest.raises(RuntimeError, match=expected):
_deprecate.deprecate(deprecated, 1, plural=plural) _deprecate.deprecate(deprecated, 1, plural=plural)

View File

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

View File

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

View File

@ -32,6 +32,7 @@ from .helper import (
is_win32, is_win32,
mark_if_feature_version, mark_if_feature_version,
skip_unless_feature, skip_unless_feature,
timeout_unless_slower_valgrind,
) )
ElementTree: ModuleType | None ElementTree: ModuleType | None
@ -144,14 +145,16 @@ class TestFileJpeg:
assert k > 0.9 assert k > 0.9
# roundtrip, and check again # roundtrip, and check again
im = self.roundtrip(im) 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 c == 0.0
assert m > 0.8 assert m > 0.8
assert y > 0.8 assert y > 0.8
assert k == 0.0 assert k == 0.0
c, m, y, k = ( cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1))
x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) assert isinstance(cmyk, tuple)
) k = cmyk[3] / 255.0
assert k > 0.9 assert k > 0.9
def test_rgb(self) -> None: def test_rgb(self) -> None:
@ -1033,7 +1036,7 @@ class TestFileJpeg:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(f, xmp=b"1" * 65505) 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: def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Even though this decoder never says that it is finished # Even though this decoder never says that it is finished
# the image should still end when there is no new data # the image should still end when there is no new data

View File

@ -37,6 +37,11 @@ def test_sanity(tmp_path: Path) -> None:
im.save(f) 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: def test_bad_image_size() -> None:
with open("Tests/images/pil184.pcx", "rb") as fp: with open("Tests/images/pil184.pcx", "rb") as fp:
data = fp.read() data = fp.read()

View File

@ -13,7 +13,12 @@ import pytest
from PIL import Image, PdfParser, features 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: 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 assert len(f.getvalue()) > initial_size
@pytest.mark.timeout(1) @timeout_unless_slower_valgrind(1)
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
@pytest.mark.parametrize("newline", (b"\r", b"\n")) @pytest.mark.parametrize("newline", (b"\r", b"\n"))
def test_redos(newline: bytes) -> None: def test_redos(newline: bytes) -> None:
malicious = b" trailer<<>>" + newline * 3456 malicious = b" trailer<<>>" + newline * 3456

View File

@ -288,12 +288,13 @@ def test_non_integer_token(tmp_path: Path) -> None:
pass 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" path = tmp_path / "temp.ppm"
with open(path, "wb") as f: 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, match="Token too long in file header: "):
with Image.open(path): with Image.open(path):
pass pass

View File

@ -28,3 +28,9 @@ def test_invalid_file() -> None:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
QoiImagePlugin.QoiImageFile(invalid_file) 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)

View File

@ -14,6 +14,7 @@ from PIL import (
ImageFile, ImageFile,
JpegImagePlugin, JpegImagePlugin,
TiffImagePlugin, TiffImagePlugin,
TiffTags,
UnidentifiedImageError, UnidentifiedImageError,
) )
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
@ -26,6 +27,7 @@ from .helper import (
hopper, hopper,
is_pypy, is_pypy,
is_win32, is_win32,
timeout_unless_slower_valgrind,
) )
ElementTree: ModuleType | None ElementTree: ModuleType | None
@ -899,6 +901,29 @@ class TestFileTiff:
assert description[0]["format"] == "image/tiff" assert description[0]["format"] == "image/tiff"
assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] 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: def test_get_photoshop_blocks(self) -> None:
with Image.open("Tests/images/lab.tif") as im: with Image.open("Tests/images/lab.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile) assert isinstance(im, TiffImagePlugin.TiffImageFile)
@ -988,7 +1013,7 @@ class TestFileTiff:
with pytest.raises(OSError): with pytest.raises(OSError):
im.load() im.load()
@pytest.mark.timeout(6) @timeout_unless_slower_valgrind(6)
@pytest.mark.filterwarnings("ignore:Truncated File Read") @pytest.mark.filterwarnings("ignore:Truncated File Read")
def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/timeout-6646305047838720") as im: with Image.open("Tests/images/timeout-6646305047838720") as im:
@ -1001,7 +1026,7 @@ class TestFileTiff:
"Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif", "Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif",
], ],
) )
@pytest.mark.timeout(2) @timeout_unless_slower_valgrind(2)
def test_oom(self, test_file: str) -> None: def test_oom(self, test_file: str) -> None:
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):
with pytest.warns(UserWarning): with pytest.warns(UserWarning):

View File

@ -34,6 +34,7 @@ from .helper import (
is_win32, is_win32,
mark_if_feature_version, mark_if_feature_version,
skip_unless_feature, skip_unless_feature,
timeout_unless_slower_valgrind,
) )
ElementTree: ModuleType | None ElementTree: ModuleType | None
@ -572,10 +573,7 @@ class TestImage:
i = Image.new("RGB", [1, 1]) i = Image.new("RGB", [1, 1])
assert isinstance(i.size, tuple) assert isinstance(i.size, tuple)
@pytest.mark.timeout(0.75) @timeout_unless_slower_valgrind(0.75)
@pytest.mark.skipif(
"PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower"
)
@pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0)))
def test_empty_image(self, size: tuple[int, int]) -> None: def test_empty_image(self, size: tuple[int, int]) -> None:
Image.new("RGB", size) Image.new("RGB", size)
@ -673,6 +671,7 @@ class TestImage:
im_remapped = im.remap_palette(list(range(256))) im_remapped = im.remap_palette(list(range(256)))
assert_image_equal(im, im_remapped) assert_image_equal(im, im_remapped)
assert im.palette is not None assert im.palette is not None
assert im_remapped.palette is not None
assert im.palette.palette == im_remapped.palette.palette assert im.palette.palette == im_remapped.palette.palette
# Test illegal image mode # Test illegal image mode
@ -975,6 +974,11 @@ class TestImage:
assert tag not in exif.get_ifd(0x8769) assert tag not in exif.get_ifd(0x8769)
assert exif.get_ifd(0xA005) 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: def test_empty_xmp(self) -> None:
with Image.open("Tests/images/hopper.gif") as im: with Image.open("Tests/images/hopper.gif") as im:
if ElementTree is None: if ElementTree is None:
@ -991,7 +995,7 @@ class TestImage:
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
im.info["xmp"] = ( im.info["xmp"] = (
b'<?xpacket begin="\xef\xbb\xbf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n' b'<?xpacket begin="\xef\xbb\xbf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n'
b'<x:xmpmeta xmlns:x="adobe:ns:meta/" />\n<?xpacket end="w"?>\x00\x00' b'<x:xmpmeta xmlns:x="adobe:ns:meta/" />\n<?xpacket end="w"?>\x00\x00 '
) )
if ElementTree is None: if ElementTree is None:
with pytest.warns( with pytest.warns(

View File

@ -783,9 +783,10 @@ def test_rectangle_I16(bbox: Coords) -> None:
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
draw.rectangle(bbox, outline=0xFFFF) draw.rectangle(bbox, outline=0xCDEF)
# Assert # Assert
assert im.getpixel((X0, Y0)) == 0xCDEF
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") 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 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()] fonts = [ImageFont.load_default_imagefont()]
if not features.check_module("freetype2"): if not features.check_module("freetype2"):
@ -72,7 +72,7 @@ def test_decompression_bomb() -> None:
font.getmask("A" * 1_000_000) font.getmask("A" * 1_000_000)
@pytest.mark.timeout(4) @timeout_unless_slower_valgrind(4)
def test_oom() -> None: def test_oom() -> None:
glyph = struct.pack( glyph = struct.pack(
">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Any # undone from typing import Any, NamedTuple
import pytest import pytest
@ -10,30 +10,73 @@ from .helper import (
assert_deep_equal, assert_deep_equal,
assert_image_equal, assert_image_equal,
hopper, hopper,
is_big_endian,
) )
pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") TYPE_CHECKING = False
if TYPE_CHECKING:
import pyarrow
else:
pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed")
TEST_IMAGE_SIZE = (10, 10) TEST_IMAGE_SIZE = (10, 10)
def _test_img_equals_pyarray( 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: ) -> None:
assert img.height * img.width == len(arr) assert img.height * img.width * elts_per_pixel == len(arr)
px = img.load() px = img.load()
assert px is not None 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 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)): for y in range(0, img.size[1], int(img.size[1] / 10)):
if mask: if mask:
for ix, elt in enumerate(mask):
pixel = px[x, y] pixel = px[x, y]
assert isinstance(pixel, tuple) 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] 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: else:
assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) 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 # really hard to get a non-nullable list type
fl_uint8_4_type = pyarrow.field( fl_uint8_4_type = pyarrow.field(
"_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4) "_", 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]), ("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) img = hopper(mode)
# Resize to non-square # Resize to non-square
img = img.crop((3, 0, 124, 127)) img = img.crop((3, 0, 124, 127))
assert img.size == (121, 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) _test_img_equals_pyarray(img, arr, mask)
assert arr.type == dtype assert arr.type == dtype
@ -79,8 +122,8 @@ def test_lifetime() -> None:
img = hopper("L") img = hopper("L")
arr_1 = pyarrow.array(img) arr_1 = pyarrow.array(img) # type: ignore[call-overload]
arr_2 = pyarrow.array(img) arr_2 = pyarrow.array(img) # type: ignore[call-overload]
del img del img
@ -97,8 +140,8 @@ def test_lifetime2() -> None:
img = hopper("L") img = hopper("L")
arr_1 = pyarrow.array(img) arr_1 = pyarrow.array(img) # type: ignore[call-overload]
arr_2 = pyarrow.array(img) arr_2 = pyarrow.array(img) # type: ignore[call-overload]
assert arr_1.sum().as_py() > 0 assert arr_1.sum().as_py() > 0
del arr_1 del arr_1
@ -110,3 +153,94 @@ def test_lifetime2() -> None:
px = img2.load() px = img2.load()
assert px # make mypy happy assert px # make mypy happy
assert isinstance(px[0, 0], int) 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") pytest.skip("test image not found")
except OSError: except OSError:
pass 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

@ -194,9 +194,9 @@ Many of Pillow's features require external libraries:
pacman -S \ pacman -S \
mingw-w64-x86_64-gcc \ mingw-w64-x86_64-gcc \
mingw-w64-x86_64-python3 \ mingw-w64-x86_64-python \
mingw-w64-x86_64-python3-pip \ mingw-w64-x86_64-python-pip \
mingw-w64-x86_64-python3-setuptools mingw-w64-x86_64-python-setuptools
Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: 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 13 Ventura | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | | 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 | | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
| | 3.12, 3.13, PyPy3 | |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, | | Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 |
| | | ppc64le, s390x | | | 3.12, 3.13, 3.14, PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 | arm64v8, ppc64le, |
| | | s390x |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Windows Server 2019 | 3.9 | x86 | | Windows Server 2022 | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+ | +----------------------------+---------------------+
| Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 | | | 3.10, 3.11, 3.12, 3.13, | x86-64 |
| | PyPy3 | | | | 3.14, PyPy3 | |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.12 (MinGW) | x86-64 | | | 3.12 (MinGW) | x86-64 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+

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 library). For earlier versions, TrueType support is only available as part of
the imToolkit package. 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:: .. warning::
To protect against potential DOS attacks when using arbitrary strings as 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 text input, Pillow will raise a :py:exc:`ValueError` if the number of characters

View File

@ -0,0 +1,56 @@
11.3.0
------
Security
========
TODO
^^^^
TODO
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards incompatible changes
==============================
TODO
^^^^
Deprecations
============
TODO
^^^^
TODO
API changes
===========
TODO
^^^^
TODO
API additions
=============
TODO
^^^^
TODO
Other changes
=============
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:: .. toctree::
:maxdepth: 2 :maxdepth: 2
11.3.0
11.2.1 11.2.1
11.1.0 11.1.0
11.0.0 11.0.0

View File

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

View File

@ -163,7 +163,7 @@ def _find_library_dirs_ldconfig() -> list[str]:
args: list[str] args: list[str]
env: dict[str, str] env: dict[str, str]
expr: str expr: str
if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): if sys.platform.startswith(("linux", "gnu")):
if struct.calcsize("l") == 4: if struct.calcsize("l") == 4:
machine = os.uname()[4] + "-32" machine = os.uname()[4] + "-32"
else: else:
@ -623,11 +623,7 @@ class pil_build_ext(build_ext):
for extension in self.extensions: for extension in self.extensions:
extension.extra_compile_args = ["-Wno-nullability-completeness"] extension.extra_compile_args = ["-Wno-nullability-completeness"]
elif ( elif sys.platform.startswith(("linux", "gnu", "freebsd")):
sys.platform.startswith("linux")
or sys.platform.startswith("gnu")
or sys.platform.startswith("freebsd")
):
for dirname in _find_library_dirs_ldconfig(): for dirname in _find_library_dirs_ldconfig():
_add_directory(library_dirs, dirname) _add_directory(library_dirs, dirname)
if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"): if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"):

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> Jerome Leclanche <jerome@leclan.ch>
Documentation: Documentation:

View File

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

View File

@ -767,18 +767,20 @@ class Image:
.. warning:: .. 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 storage. For compressed image data (e.g. PNG, JPEG) use
:meth:`~.save`, with a BytesIO parameter for in-memory :meth:`~.save`, with a BytesIO parameter for in-memory data.
data.
:param encoder_name: What encoder to use. The default is to :param encoder_name: What encoder to use.
use the standard "raw" encoder.
A list of C encoders can be seen under The default is to use the standard "raw" encoder.
codecs section of the function array in To see how this packs pixel data into the returned
:file:`_imaging.c`. Python encoders are bytes, see :file:`libImaging/Pack.c`.
registered within the relevant plugins.
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. :param args: Extra arguments to the encoder.
:returns: A :py:class:`bytes` object. :returns: A :py:class:`bytes` object.
""" """
@ -800,7 +802,9 @@ class Image:
e = _getencoder(self.mode, encoder_name, encoder_args) e = _getencoder(self.mode, encoder_name, encoder_args)
e.setimage(self.im) 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 = [] output = []
while True: while True:
@ -1507,7 +1511,7 @@ class Image:
return {} return {}
if "xmp" not in self.info: if "xmp" not in self.info:
return {} return {}
root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00")) root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00 "))
return {get_name(root.tag): get_value(root)} return {get_name(root.tag): get_value(root)}
def getexif(self) -> Exif: def getexif(self) -> Exif:
@ -1538,10 +1542,11 @@ class Image:
# XMP tags # XMP tags
if ExifTags.Base.Orientation not in self._exif: if ExifTags.Base.Orientation not in self._exif:
xmp_tags = self.info.get("XML:com.adobe.xmp") 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")): 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: if xmp_tags:
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) match = re.search(pattern, xmp_tags)
if match: if match:
self._exif[ExifTags.Base.Orientation] = int(match[2]) self._exif[ExifTags.Base.Orientation] = int(match[2])

View File

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

View File

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

View File

@ -762,8 +762,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
extra = info.get("extra", b"") extra = info.get("extra", b"")
MAX_BYTES_IN_MARKER = 65533 MAX_BYTES_IN_MARKER = 65533
xmp = info.get("xmp") if xmp := info.get("xmp"):
if xmp:
overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
if len(xmp) > max_data_bytes_in_marker: 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)) size = o16(2 + overhead_len + len(xmp))
extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + 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 := info.get("icc_profile"):
if icc_profile:
overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers))
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
markers = [] 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 # 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. # channels*size, this is a value that's been used in a django patch.
# https://github.com/matthewwithanm/django-imagekit/issues/50 # https://github.com/matthewwithanm/django-imagekit/issues/50
bufsize = 0
if optimize or progressive: if optimize or progressive:
# CMYK can be bigger # CMYK can be bigger
if im.mode == "CMYK": if im.mode == "CMYK":
@ -848,7 +845,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
else: else:
# The EXIF info needs to be written as one block, + APP1, + one spare byte. # 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. # 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( ImageFile._save(
im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize 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: def peek(self, bits: int) -> int:
while self.bits < bits: while self.bits < bits:
c = self.next() self.bitbuffer = (self.bitbuffer << 8) + self.next()
if c < 0:
self.bits = 0
continue
self.bitbuffer = (self.bitbuffer << 8) + c
self.bits += 8 self.bits += 8
return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1

View File

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

View File

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

View File

@ -51,7 +51,7 @@ class QoiDecoder(ImageFile.PyDecoder):
assert self.fd is not None assert self.fd is not None
self._previously_seen_pixels = {} self._previously_seen_pixels = {}
self._add_to_previous_pixels(bytearray((0, 0, 0, 255))) self._previous_pixel = bytearray((0, 0, 0, 255))
data = bytearray() data = bytearray()
bands = Image.getmodebands(self.mode) bands = Image.getmodebands(self.mode)

View File

@ -1217,9 +1217,10 @@ class TiffImageFile(ImageFile.ImageFile):
return return
self._seek(frame) self._seek(frame)
if self._im is not None and ( 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 self._im = None
def _seek(self, frame: int) -> None: def _seek(self, frame: int) -> None:
@ -1259,7 +1260,10 @@ class TiffImageFile(ImageFile.ImageFile):
self.fp.seek(self._frame_pos[frame]) self.fp.seek(self._frame_pos[frame])
self.tag_v2.load(self.fp) self.tag_v2.load(self.fp)
if XMP in self.tag_v2: 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: elif "xmp" in self.info:
del self.info["xmp"] del self.info["xmp"]
self._reload_exif() self._reload_exif()

View File

@ -308,8 +308,8 @@ _new_arrow(PyObject *self, PyObject *args) {
} }
// ImagingBorrowArrow is responsible for retaining the array_capsule // ImagingBorrowArrow is responsible for retaining the array_capsule
ret = ret = PyImagingNew(
PyImagingNew(ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule) ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule)
); );
if (!ret) { if (!ret) {
return ImagingError_ValueError("Invalid Arrow array mode or size mismatch"); return ImagingError_ValueError("Invalid Arrow array mode or size mismatch");
@ -1665,7 +1665,8 @@ _putdata(ImagingObject *self, PyObject *args) {
int bigendian = 0; int bigendian = 0;
if (image->type == IMAGING_TYPE_SPECIAL) { if (image->type == IMAGING_TYPE_SPECIAL) {
// I;16* // I;16*
if (strcmp(image->mode, "I;16B") == 0 if (
strcmp(image->mode, "I;16B") == 0
#ifdef WORDS_BIGENDIAN #ifdef WORDS_BIGENDIAN
|| strcmp(image->mode, "I;16N") == 0 || strcmp(image->mode, "I;16N") == 0
#endif #endif
@ -2226,6 +2227,7 @@ _unsharp_mask(ImagingObject *self, PyObject *args) {
} }
if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) { if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) {
ImagingDelete(imOut);
return NULL; return NULL;
} }
@ -3218,7 +3220,8 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) {
(int)p[3], (int)p[3],
&ink, &ink,
width, width,
self->blend self->blend,
NULL
) < 0) { ) < 0) {
free(xy); free(xy);
return NULL; return NULL;
@ -3356,7 +3359,10 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) {
int ink; int ink;
int fill = 0; int fill = 0;
int width = 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; return NULL;
} }
@ -3386,8 +3392,16 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) {
free(xy); free(xy);
if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) < if (ImagingDrawPolygon(
0) { self->image->image,
n,
ixy,
&ink,
fill,
width,
self->blend,
maskp ? maskp->image : NULL
) < 0) {
free(ixy); free(ixy);
return NULL; return NULL;
} }

View File

@ -275,6 +275,7 @@ text_layout_raqm(
if (!text || !size) { if (!text || !size) {
/* return 0 and clean up, no glyphs==no size, /* return 0 and clean up, no glyphs==no size,
and raqm fails with empty strings */ and raqm fails with empty strings */
PyMem_Free(text);
goto failed; goto failed;
} }
set_text = raqm_set_text(rq, text, size); 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 " "setting text direction, language or font features is not supported "
"without libraqm" "without libraqm"
); );
return 0;
} }
if (PyUnicode_Check(string)) { if (PyUnicode_Check(string)) {

View File

@ -641,6 +641,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
ImagingSectionLeave(&cookie); ImagingSectionLeave(&cookie);
WebPPictureFree(&pic); WebPPictureFree(&pic);
output = writer.mem;
ret_size = writer.size;
if (!ok) { if (!ok) {
int error_code = (&pic)->error_code; int error_code = (&pic)->error_code;
char message[50] = ""; char message[50] = "";
@ -652,10 +656,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
); );
} }
PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message);
free(output);
return NULL; return NULL;
} }
output = writer.mem;
ret_size = writer.size;
{ {
/* I want to truncate the *_size items that get passed into WebP /* I want to truncate the *_size items that get passed into WebP

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -510,7 +510,15 @@ extern int
ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink, int op); ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink, int op);
extern int extern int
ImagingDrawWideLine( 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 extern int
ImagingDrawPieslice( ImagingDrawPieslice(
@ -530,7 +538,14 @@ extern int
ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op); ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op);
extern int extern int
ImagingDrawPolygon( 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 extern int
ImagingDrawRectangle( 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) { if (params->cp_cinema == OPJ_CINEMA4K_24) {
float max_rate = 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].tile = 1;
params->POC[0].resno0 = 0; 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; params->max_comp_size = COMP_24_CS_MAX_LENGTH;
} else { } else {
float max_rate = 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) { for (n = 0; n < params->tcp_numlayers; ++n) {
rate = 0; rate = 0;

View File

@ -131,6 +131,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
break; break;
default: default:
state->errcode = IMAGING_CODEC_CONFIG; state->errcode = IMAGING_CODEC_CONFIG;
jpeg_destroy_compress(&context->cinfo);
return -1; return -1;
} }
@ -161,6 +162,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
/* Would subsample the green and blue /* Would subsample the green and blue
channels, which doesn't make sense */ channels, which doesn't make sense */
state->errcode = IMAGING_CODEC_CONFIG; state->errcode = IMAGING_CODEC_CONFIG;
jpeg_destroy_compress(&context->cinfo);
return -1; return -1;
} }
jpeg_set_colorspace(&context->cinfo, JCS_RGB); 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->x >= state->bytes) {
if (state->bytes % state->xsize && state->bytes > state->xsize) { int bands;
int bands = state->bytes / state->xsize; int xsize = 0;
int stride = state->bytes / bands; 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; int i;
for (i = 1; i < bands; i++) { // note -- skipping first band for (i = 1; i < bands; i++) { // note -- skipping first band
memmove( memmove(
&state->buffer[i * state->xsize], &state->buffer[i * xsize], &state->buffer[i * stride], xsize
&state->buffer[i * stride],
state->xsize
); );
} }
} }

View File

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

View File

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

View File

@ -602,8 +602,9 @@ ImagingBorrowArrow(
} }
if (!borrowed_buffer) { if (!borrowed_buffer) {
return (Imaging return (Imaging)ImagingError_ValueError(
)ImagingError_ValueError("Arrow Array, exactly 2 buffers required"); "Arrow Array, exactly 2 buffers required"
);
} }
for (y = i = 0; y < im->ysize; y++) { for (y = i = 0; y < im->ysize; y++) {
@ -723,6 +724,8 @@ ImagingNewArrow(
int64_t pixels = (int64_t)xsize * (int64_t)ysize; int64_t pixels = (int64_t)xsize * (int64_t)ysize;
// fmt:off // don't reformat this // 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 if (((strcmp(schema->format, "I") == 0 // int32
&& im->pixelsize == 4 // 4xchar* storage && im->pixelsize == 4 // 4xchar* storage
&& im->bands >= 2) // INT32 into any INT32 Storage mode && im->bands >= 2) // INT32 into any INT32 Storage mode
@ -735,6 +738,7 @@ ImagingNewArrow(
return im; return im;
} }
} }
// Stored as [[r,g,b,a],...]
if (strcmp(schema->format, "+w:4") == 0 // 4 up array if (strcmp(schema->format, "+w:4") == 0 // 4 up array
&& im->pixelsize == 4 // storage as 32 bpc && im->pixelsize == 4 // storage as 32 bpc
&& schema->n_children > 0 // make sure schema is well formed. && schema->n_children > 0 // make sure schema is well formed.
@ -750,6 +754,17 @@ ImagingNewArrow(
return im; 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 // fmt: on
ImagingDelete(im); ImagingDelete(im);
return NULL; return NULL;

View File

@ -557,7 +557,8 @@ _decodeStrip(
(tdata_t)state->buffer, (tdata_t)state->buffer,
strip_size strip_size
) == -1) { ) == -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; state->errcode = IMAGING_CODEC_BROKEN;
return -1; return -1;
@ -929,6 +930,27 @@ ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) {
return status; 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 int
ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) { ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes) {
/* One shot encoder. Encode everything to the tiff in the clientstate. /* One shot encoder. Encode everything to the tiff in the clientstate.
@ -1010,16 +1032,6 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
TRACE(("Encode Error, row %d\n", state->y)); TRACE(("Encode Error, row %d\n", state->y));
state->errcode = IMAGING_CODEC_BROKEN; 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);
} 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) { if (!clientstate->fp) {
free(clientstate->data); free(clientstate->data);
} }
@ -1036,22 +1048,11 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
TRACE(("Error flushing the tiff")); TRACE(("Error flushing the tiff"));
// likely reason is memory. // likely reason is memory.
state->errcode = IMAGING_CODEC_MEMORY; state->errcode = IMAGING_CODEC_MEMORY;
if (clientstate->fp) {
TIFFCleanup(tiff);
} else {
TIFFClose(tiff);
}
if (!clientstate->fp) { if (!clientstate->fp) {
free(clientstate->data); free(clientstate->data);
} }
return -1; 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. // reset the clientstate metadata to use it to read out the buffer.
clientstate->loc = 0; clientstate->loc = 0;
clientstate->size = clientstate->eof; // redundant? clientstate->size = clientstate->eof; // redundant?

View File

@ -40,6 +40,8 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset);
extern int extern int
ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp);
extern int extern int
ImagingLibTiffEncodeCleanup(ImagingCodecState state);
extern int
ImagingLibTiffMergeFieldInfo( ImagingLibTiffMergeFieldInfo(
ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length 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; im->destroy = mapping_destroy_buffer;
Py_INCREF(target); Py_INCREF(target);

View File

@ -3,7 +3,7 @@ requires =
tox>=4.2 tox>=4.2
env_list = env_list =
lint lint
py{py3, 313, 312, 311, 310, 39} py{py3, 314, 313, 312, 311, 310, 39}
[testenv] [testenv]
deps = 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 Microsoft Visual Studio 2017 or newer with C++ component.
* Requires NASM for libjpeg-turbo, a required dependency when using this script. * Requires NASM for libjpeg-turbo, a required dependency when using this script.
* Requires CMake 3.15 or newer (available as Visual Studio component). * Requires CMake 3.15 or newer (available as Visual Studio component).
* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise and Windows Server * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions).
2019 with Visual Studio 2019 Enterprise (GitHub Actions).
Here's an example script to build on Windows: Here's an example script to build on Windows:

View File

@ -117,7 +117,7 @@ V = {
"FREETYPE": "2.13.3", "FREETYPE": "2.13.3",
"FRIBIDI": "1.0.16", "FRIBIDI": "1.0.16",
"HARFBUZZ": "11.2.1", "HARFBUZZ": "11.2.1",
"JPEGTURBO": "3.1.0", "JPEGTURBO": "3.1.1",
"LCMS2": "2.17", "LCMS2": "2.17",
"LIBAVIF": "1.3.0", "LIBAVIF": "1.3.0",
"LIBIMAGEQUANT": "4.3.4", "LIBIMAGEQUANT": "4.3.4",