diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 887c7dd96..8d8cb0b15 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -72,10 +72,10 @@ jobs: - name: Install dependencies id: install run: | - 7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\" - echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH + choco install nasm --no-progress + echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - choco install ghostscript --version=10.0.0.20230317 + choco install ghostscript --version=10.0.0.20230317 --no-progress echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH # Install extra test images @@ -167,7 +167,6 @@ jobs: - name: Build Pillow run: | $FLAGS="-C raqm=vendor -C fribidi=vendor" - if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS+=" -C imagequant=disable" } cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ." & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh @@ -209,47 +208,6 @@ jobs: flags: GHA_Windows name: ${{ runner.os }} Python ${{ matrix.python-version }} - - name: Build wheel - id: wheel - if: "github.event_name != 'pull_request'" - run: | - mkdir fribidi - copy winbuild\build\bin\fribidi* fribidi - setlocal EnableDelayedExpansion - for %%f in (winbuild\build\license\*) do ( - set x=%%~nf - rem Skip FriBiDi license, it is not included in the wheel. - set fribidi=!x:~0,7! - if NOT !fribidi!==fribidi ( - rem Skip imagequant license, it is not included in the wheel. - set libimagequant=!x:~0,13! - if NOT !libimagequant!==libimagequant ( - echo. >> LICENSE - echo ===== %%~nf ===== >> LICENSE - echo. >> LICENSE - type %%f >> LICENSE - ) - ) - ) - for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT% - call winbuild\\build\\build_env.cmd - %pythonLocation%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor -C imagequant=disable . - shell: cmd - - - name: Upload wheel - uses: actions/upload-artifact@v3 - if: "github.event_name != 'pull_request'" - with: - name: ${{ steps.wheel.outputs.dist }} - path: "*.whl" - - - name: Upload fribidi.dll - if: "github.event_name != 'pull_request' && matrix.python-version == 3.11" - uses: actions/upload-artifact@v3 - with: - name: fribidi - path: fribidi\* - success: permissions: contents: none diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4b9a4d46b..3ec314873 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -16,13 +16,13 @@ ARCHIVE_SDIR=pillow-depends-main # Package versions for fresh source builds FREETYPE_VERSION=2.13.2 -HARFBUZZ_VERSION=8.2.1 +HARFBUZZ_VERSION=8.3.0 LIBPNG_VERSION=1.6.40 JPEGTURBO_VERSION=3.0.1 OPENJPEG_VERSION=2.5.0 XZ_VERSION=5.4.5 TIFF_VERSION=4.6.0 -LCMS2_VERSION=2.15 +LCMS2_VERSION=2.16 if [[ -n "$IS_MACOS" ]]; then GIFLIB_VERSION=5.1.4 else diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 new file mode 100644 index 000000000..f593c7228 --- /dev/null +++ b/.github/workflows/wheels-test.ps1 @@ -0,0 +1,22 @@ +param ([string]$venv, [string]$pillow="C:\pillow") +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +Set-PSDebug -Trace 1 +if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") { + # unlike CPython, PyPy requires Visual C++ Redistributable to be installed + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri 'https://aka.ms/vs/15/release/vc_redist.x64.exe' -OutFile 'vc_redist.x64.exe' + C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null +} +$env:path += ";$pillow\winbuild\build\bin\" +& "$venv\Scripts\activate.ps1" +& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f +cd $pillow +& python -VV +if (!$?) { exit $LASTEXITCODE } +& python selftest.py +if (!$?) { exit $LASTEXITCODE } +& python -m pytest -vx Tests\check_wheel.py +if (!$?) { exit $LASTEXITCODE } +& python -m pytest -vx Tests +if (!$?) { exit $LASTEXITCODE } diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 34dfeaf14..207ec1567 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -1,10 +1,6 @@ #!/bin/bash set -e -EXP_CODECS="jpg jpg_2000 libtiff zlib" -EXP_MODULES="freetype2 littlecms2 pil tkinter webp" -EXP_FEATURES="fribidi harfbuzz libjpeg_turbo raqm transp_webp webp_anim webp_mux xcb" - if [[ "$OSTYPE" == "darwin"* ]]; then brew install fribidi export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" @@ -25,21 +21,5 @@ fi # Runs tests python3 selftest.py +python3 -m pytest Tests/check_wheel.py python3 -m pytest - -# Test against expected codecs, modules and features -codecs=$(python3 -c 'from PIL.features import *; print(" ".join(sorted(get_supported_codecs())))') -if [ "$codecs" != "$EXP_CODECS" ]; then - echo "Codecs should be: '$EXP_CODECS'; but are '$codecs'" - exit 1 -fi -modules=$(python3 -c 'from PIL.features import *; print(" ".join(sorted(get_supported_modules())))') -if [ "$modules" != "$EXP_MODULES" ]; then - echo "Modules should be: '$EXP_MODULES'; but are '$modules'" - exit 1 -fi -features=$(python3 -c 'from PIL.features import *; print(" ".join(sorted(get_supported_features())))') -if [ "$features" != "$EXP_FEATURES" ]; then - echo "Features should be: '$EXP_FEATURES'; but are '$features'" - exit 1 -fi diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 1b141b14f..c4737bfc7 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -3,14 +3,20 @@ name: Wheels on: push: paths: - - ".github/workflows/wheels*.yml" + - ".ci/requirements-cibw.txt" + - ".github/workflows/wheel*" - "wheels/*" + - "winbuild/build_prepare.py" + - "winbuild/fribidi.cmake" tags: - "*" pull_request: paths: - - ".github/workflows/wheels*.yml" + - ".ci/requirements-cibw.txt" + - ".github/workflows/wheel*" - "wheels/*" + - "winbuild/build_prepare.py" + - "winbuild/fribidi.cmake" workflow_dispatch: permissions: @@ -63,7 +69,6 @@ jobs: env: CIBW_ARCHS: ${{ matrix.archs }} CIBW_BUILD: ${{ matrix.build }} - CIBW_CONFIG_SETTINGS: raqm=enable raqm=vendor fribidi=vendor CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_SKIP: pp38-* @@ -75,6 +80,102 @@ jobs: name: dist path: ./wheelhouse/*.whl + windows: + name: Windows ${{ matrix.arch }} + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - arch: x86 + cibw_arch: x86 + - arch: x64 + cibw_arch: AMD64 + - arch: ARM64 + cibw_arch: ARM64 + steps: + - uses: actions/checkout@v4 + + - name: Checkout extra test images + uses: actions/checkout@v4 + with: + repository: python-pillow/test-images + path: Tests\test-images + + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - name: Prepare for build + run: | + choco install nasm --no-progress + echo "C:\Program Files\NASM" >> $env:GITHUB_PATH + + # Install extra test images + xcopy /S /Y Tests\test-images\* Tests\images + + & python.exe -m pip install -r .ci/requirements-cibw.txt + + # Cannot cross-compile FriBiDi (only used for tests) + $FLAGS = ("--no-imagequant", "--architecture=${{ matrix.arch }}") + if ('${{ matrix.arch }}' -eq 'ARM64') { $FLAGS += "--no-fribidi" } + & python.exe winbuild\build_prepare.py -v @FLAGS + shell: pwsh + + - name: Build wheels + run: | + setlocal EnableDelayedExpansion + for %%f in (winbuild\build\license\*) do ( + set x=%%~nf + rem Skip FriBiDi license, it is not included in the wheel. + set fribidi=!x:~0,7! + if NOT !fribidi!==fribidi ( + rem Skip imagequant license, it is not included in the wheel. + set libimagequant=!x:~0,13! + if NOT !libimagequant!==libimagequant ( + echo. >> LICENSE + echo ===== %%~nf ===== >> LICENSE + echo. >> LICENSE + type %%f >> LICENSE + ) + ) + ) + call winbuild\\build\\build_env.cmd + %pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse + env: + CIBW_ARCHS: ${{ matrix.cibw_arch }} + CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" + CIBW_CACHE_PATH: "C:\\cibw" + CIBW_TEST_SKIP: "*-win_arm64" + CIBW_TEST_COMMAND: 'docker run --rm + -v {project}:C:\pillow + -v C:\cibw:C:\cibw + -v %CD%\..\venv-test:%CD%\..\venv-test + -e CI -e GITHUB_ACTIONS + mcr.microsoft.com/windows/servercore:ltsc2022 + powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test' + shell: cmd + + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: dist + path: ./wheelhouse/*.whl + + - name: Prepare to upload FriBiDi + if: "matrix.arch != 'ARM64'" + run: | + mkdir fribidi\${{ matrix.arch }} + copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }} + shell: cmd + + - name: Upload fribidi.dll + if: "matrix.arch != 'ARM64'" + uses: actions/upload-artifact@v3 + with: + name: fribidi + path: fribidi\* + sdist: runs-on: ubuntu-latest steps: @@ -97,7 +198,7 @@ jobs: success: permissions: contents: none - needs: [build, sdist] + needs: [build, windows, sdist] runs-on: ubuntu-latest name: Wheels Successful steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b2dc06ae..f6b8c349c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.4 + rev: v0.1.6 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black @@ -42,12 +42,12 @@ repos: exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v0.8.1 + rev: v0.9.0 hooks: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.4.1 + rev: 1.5.3 hooks: - id: pyproject-fmt diff --git a/.travis.yml b/.travis.yml index 4005dbcf0..8f8250809 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ if: tag IS present OR type = api env: global: - CIBW_ARCHS=aarch64 - - CIBW_CONFIG_SETTINGS="raqm=enable raqm=vendor fribidi=vendor" - CIBW_SKIP=pp38-* language: python diff --git a/CHANGES.rst b/CHANGES.rst index 251917654..2ace41d22 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,25 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- -- Raise ValueError when TrueType font size is not greater than zero #7584 +- Optimize ImageStat.Stat.extrema #7593 + [florath, radarhere] + +- Handle pathlib.Path in FreeTypeFont #7578 + [radarhere, hugovk, nulano] + +- Added support for reading DX10 BC4 DDS images #7603 + [sambvfx, radarhere] + +- Optimized ImageStat.Stat.count #7599 + [florath] + +- Correct PDF palette size when saving #7555 + [radarhere] + +- Fixed closing file pointer with olefile 0.47 #7594 + [radarhere] + +- Raise ValueError when TrueType font size is not greater than zero #7584, #7587 [akx, radarhere] - If absent, do not try to close fp when closing image #7557 diff --git a/RELEASING.md b/RELEASING.md index 8b0673203..74f427f03 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -94,7 +94,6 @@ Released as needed privately to individual vendors for critical security-related ## Source and Binary Distributions -### macOS and Linux * [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): ```bash @@ -104,14 +103,6 @@ Released as needed privately to individual vendors for critical security-related * [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases) and copy into `dist`. -### Windows -* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) - and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): - ```bash - gh run download --dir dist - # select dist-x.y.z - ``` - ## Publicize Release * [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py new file mode 100644 index 000000000..cc52cb75e --- /dev/null +++ b/Tests/check_wheel.py @@ -0,0 +1,41 @@ +import sys + +from PIL import features + + +def test_wheel_modules(): + expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} + + # tkinter is not available in cibuildwheel installed CPython on Windows + try: + import tkinter + + assert tkinter + except ImportError: + expected_modules.remove("tkinter") + + assert set(features.get_supported_modules()) == expected_modules + + +def test_wheel_codecs(): + expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"} + + assert set(features.get_supported_codecs()) == expected_codecs + + +def test_wheel_features(): + expected_features = { + "webp_anim", + "webp_mux", + "transp_webp", + "raqm", + "fribidi", + "harfbuzz", + "libjpeg_turbo", + "xcb", + } + + if sys.platform == "win32": + expected_features.remove("xcb") + + assert set(features.get_supported_features()) == expected_features diff --git a/Tests/helper.py b/Tests/helper.py index aacd95564..cce7eca3a 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -5,6 +5,7 @@ Helper functions. import logging import os import shutil +import subprocess import sys import sysconfig import tempfile @@ -258,11 +259,21 @@ def hopper(mode=None, cache={}): def djpeg_available(): - return bool(shutil.which("djpeg")) + if shutil.which("djpeg"): + try: + subprocess.check_call(["djpeg", "-version"]) + return True + except subprocess.CalledProcessError: # pragma: no cover + return False def cjpeg_available(): - return bool(shutil.which("cjpeg")) + if shutil.which("cjpeg"): + try: + subprocess.check_call(["cjpeg", "-version"]) + return True + except subprocess.CalledProcessError: # pragma: no cover + return False def netpbm_available(): diff --git a/Tests/images/bc1.dds b/Tests/images/bc1.dds new file mode 100755 index 000000000..faec63a00 Binary files /dev/null and b/Tests/images/bc1.dds differ diff --git a/Tests/images/bc1_typeless.dds b/Tests/images/bc1_typeless.dds new file mode 100755 index 000000000..47a85e2d0 Binary files /dev/null and b/Tests/images/bc1_typeless.dds differ diff --git a/Tests/images/bc4_typeless.dds b/Tests/images/bc4_typeless.dds new file mode 100644 index 000000000..27f87889f Binary files /dev/null and b/Tests/images/bc4_typeless.dds differ diff --git a/Tests/images/bc4_unorm.dds b/Tests/images/bc4_unorm.dds new file mode 100644 index 000000000..13da711bd Binary files /dev/null and b/Tests/images/bc4_unorm.dds differ diff --git a/Tests/images/bc4_unorm.png b/Tests/images/bc4_unorm.png new file mode 100644 index 000000000..71d536c84 Binary files /dev/null and b/Tests/images/bc4_unorm.png differ diff --git a/Tests/images/bc4u.dds b/Tests/images/bc4u.dds new file mode 100644 index 000000000..7f9f050b6 Binary files /dev/null and b/Tests/images/bc4u.dds differ diff --git a/Tests/images/unimplemented_pixel_format.dds b/Tests/images/unimplemented_pfflags.dds similarity index 99% rename from Tests/images/unimplemented_pixel_format.dds rename to Tests/images/unimplemented_pfflags.dds index 41a343886..e3fc8344d 100755 Binary files a/Tests/images/unimplemented_pixel_format.dds and b/Tests/images/unimplemented_pfflags.dds differ diff --git a/Tests/images/unsupported_bitcount.dds b/Tests/images/unsupported_bitcount.dds new file mode 100644 index 000000000..f9bb82254 Binary files /dev/null and b/Tests/images/unsupported_bitcount.dds differ diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index d0c81b5e9..1fb97a789 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -356,9 +356,7 @@ def test_apng_save(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/single_frame_default.png") as im: - frames = [] - for frame_im in ImageSequence.Iterator(im): - frames.append(frame_im.copy()) + frames = [frame_im.copy() for frame_im in ImageSequence.Iterator(im)] frames[0].save( test_file, save_all=True, default_image=True, append_images=frames[1:] ) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 7772956af..78cf758d3 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -12,9 +12,14 @@ TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" TEST_FILE_ATI1 = "Tests/images/ati1.dds" TEST_FILE_ATI2 = "Tests/images/ati2.dds" +TEST_FILE_DX10_BC4_TYPELESS = "Tests/images/bc4_typeless.dds" +TEST_FILE_DX10_BC4_UNORM = "Tests/images/bc4_unorm.dds" TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds" TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" +TEST_FILE_DX10_BC1 = "Tests/images/bc1.dds" +TEST_FILE_DX10_BC1_TYPELESS = "Tests/images/bc1_typeless.dds" +TEST_FILE_BC4U = "Tests/images/bc4u.dds" TEST_FILE_BC5S = "Tests/images/bc5s.dds" TEST_FILE_BC5U = "Tests/images/bc5u.dds" TEST_FILE_BC6H = "Tests/images/bc6h.dds" @@ -30,11 +35,20 @@ TEST_FILE_UNCOMPRESSED_BGR15 = "Tests/images/bgr15.dds" TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" -def test_sanity_dxt1(): - """Check DXT1 images can be opened""" +@pytest.mark.parametrize( + "image_path", + ( + TEST_FILE_DXT1, + # hexeditted to use DX10 FourCC + TEST_FILE_DX10_BC1, + TEST_FILE_DX10_BC1_TYPELESS, + ), +) +def test_sanity_dxt1_bc1(image_path): + """Check DXT1 and BC1 images can be opened""" with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: target = target.convert("RGBA") - with Image.open(TEST_FILE_DXT1) as im: + with Image.open(image_path) as im: im.load() assert im.format == "DDS" @@ -70,10 +84,18 @@ def test_sanity_dxt5(): assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png")) -def test_sanity_ati1(): - """Check ATI1 images can be opened""" +@pytest.mark.parametrize( + "image_path", + ( + TEST_FILE_ATI1, + # hexeditted to use BC4U FourCC + TEST_FILE_BC4U, + ), +) +def test_sanity_ati1_bc4u(image_path): + """Check ATI1 and BC4U images can be opened""" - with Image.open(TEST_FILE_ATI1) as im: + with Image.open(image_path) as im: im.load() assert im.format == "DDS" @@ -83,6 +105,27 @@ def test_sanity_ati1(): assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) +@pytest.mark.parametrize( + "image_path", + ( + TEST_FILE_DX10_BC4_UNORM, + # hexeditted to be typeless + TEST_FILE_DX10_BC4_TYPELESS, + ), +) +def test_dx10_bc4(image_path): + """Check DX10 BC4 images can be opened""" + + with Image.open(image_path) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "L" + assert im.size == (64, 64) + + assert_image_equal_tofile(im, TEST_FILE_DX10_BC4_UNORM.replace(".dds", ".png")) + + @pytest.mark.parametrize( "image_path", ( @@ -200,12 +243,6 @@ def test_dx10_r8g8b8a8_unorm_srgb(): ) -def test_unimplemented_dxgi_format(): - with pytest.raises(NotImplementedError): - with Image.open("Tests/images/unimplemented_dxgi_format.dds"): - pass - - @pytest.mark.parametrize( ("mode", "size", "test_file"), [ @@ -305,9 +342,22 @@ def test_palette(): assert_image_equal_tofile(im, "Tests/images/transparent.gif") -def test_unimplemented_pixel_format(): +def test_unsupported_bitcount(): + with pytest.raises(OSError): + with Image.open("Tests/images/unsupported_bitcount.dds"): + pass + + +@pytest.mark.parametrize( + "test_file", + ( + "Tests/images/unimplemented_dxgi_format.dds", + "Tests/images/unimplemented_pfflags.dds", + ), +) +def test_not_implemented(test_file): with pytest.raises(NotImplementedError): - with Image.open("Tests/images/unimplemented_pixel_format.dds"): + with Image.open(test_file): pass diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 981753eb9..3bafc4c9c 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -67,7 +67,7 @@ def test_quantize_no_dither(): def test_quantize_no_dither2(): im = Image.new("RGB", (9, 1)) - im.putdata(list((p,) * 3 for p in range(0, 36, 4))) + im.putdata([(p,) * 3 for p in range(0, 36, 4)]) palette = Image.new("P", (1, 1)) data = (0, 0, 0, 32, 32, 32) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index e21bf8cbf..0f1c52b66 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -4,6 +4,7 @@ import re import shutil import sys from io import BytesIO +from pathlib import Path import pytest from packaging.version import parse as parse_version @@ -76,8 +77,9 @@ def _render(font, layout_engine): return img -def test_font_with_name(layout_engine): - _render(FONT_PATH, layout_engine) +@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH))) +def test_font_with_name(layout_engine, font): + _render(font, layout_engine) def test_font_with_filelike(layout_engine): diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index f8059eca4..a75cbadc4 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -11,6 +11,10 @@ from .helper import assert_image_equal_tofile, skip_unless_feature class TestImageGrab: + @pytest.mark.skipif( + os.environ.get("USERNAME") == "ContainerAdministrator", + reason="can't grab screen when running in Docker", + ) @pytest.mark.skipif( sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" ) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 38c00f870..23da312a6 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -521,6 +521,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 2.5.0 +**streamtype** + Allows storing images without quantization and Huffman tables, or with + these tables but without image data. This is useful for container formats + or network protocols that handle tables separately and share them between + images. + + * ``0`` (default): interchange datastream, with tables and image data + * ``1``: abbreviated table specification (tables-only) datastream + + .. versionadded:: 10.2.0 + + * ``2``: abbreviated image (image-only) datastream + **comment** A comment about the image. diff --git a/docs/installation.rst b/docs/installation.rst index 78900aa57..fbcfbb907 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -95,11 +95,10 @@ and :pypi:`olefile` for Pillow to read FPX and MIC images:: .. tab:: Windows - .. warning:: Pillow > 9.5.0 no longer includes 32-bit wheels. - - We provide Pillow binaries for Windows compiled for the matrix of - supported Pythons in 64-bit versions in the wheel format. These binaries include - support for all optional libraries except libimagequant and libxcb. Raqm support + We provide Pillow binaries for Windows compiled for the matrix of supported + Pythons in the wheel format. These include x86, x86-64 and arm64 versions + (with the exception of Python 3.8 on arm64). These binaries include support + for all optional libraries except libimagequant and libxcb. Raqm support requires FriBiDi to be installed separately:: python3 -m pip install --upgrade pip @@ -176,7 +175,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.15**. + above uses liblcms2. Tested with **1.19** and **2.7-2.16**. * **libwebp** provides the WebP format. diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index a944a13fa..6edf4b05c 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -70,21 +70,20 @@ Methods Constants --------- -.. data:: PIL.ImageFont.Layout.BASIC +.. class:: Layout - Use basic text layout for TrueType font. - Advanced features such as text direction are not supported. + .. py:attribute:: BASIC -.. data:: PIL.ImageFont.Layout.RAQM + Use basic text layout for TrueType font. + Advanced features such as text direction are not supported. - Use Raqm text layout for TrueType font. - Advanced features are supported. + .. py:attribute:: RAQM - Requires Raqm, you can check support using - :py:func:`PIL.features.check_feature` with ``feature="raqm"``. + Use Raqm text layout for TrueType font. + Advanced features are supported. -Constants ---------- + Requires Raqm, you can check support using + :py:func:`PIL.features.check_feature` with ``feature="raqm"``. .. data:: MAX_STRING_LENGTH diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index fcf4514a8..18cd99cf3 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -33,6 +33,14 @@ Plugin reference :undoc-members: :show-inheritance: +:mod:`~PIL.DdsImagePlugin` Module +--------------------------------- + +.. automodule:: PIL.DdsImagePlugin + :members: + :undoc-members: + :show-inheritance: + :mod:`~PIL.EpsImagePlugin` Module --------------------------------- diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst new file mode 100644 index 000000000..d507ed737 --- /dev/null +++ b/docs/releasenotes/10.2.0.rst @@ -0,0 +1,56 @@ +10.2.0 +------ + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +TODO + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Added DdsImagePlugin enums +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:class:`~PIL.DdsImagePlugin.DDSD`, :py:class:`~PIL.DdsImagePlugin.DDSCAPS`, +:py:class:`~PIL.DdsImagePlugin.DDSCAPS2`, :py:class:`~PIL.DdsImagePlugin.DDPF`, +:py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT` +enums have been added to :py:class:`PIL.DdsImagePlugin`. + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +Added DDS BC4U and DX10 BC1 and BC4 reading +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added to read the BC4U format of DDS images. + +Support has also been added to read DX10 BC1 and BC4, whether UNORM or +TYPELESS. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 1b1c353fd..d8034853c 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.2.0 10.1.0 10.0.1 10.0.0 diff --git a/pyproject.toml b/pyproject.toml index 81a51f0d8..f9cea0612 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,12 +88,15 @@ version = {attr = "PIL.__version__"} [tool.cibuildwheel] before-all = ".github/workflows/wheels-dependencies.sh" +build-verbosity = 1 +config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" [tool.ruff] line-length = 88 select = [ + "C4", # flake8-comprehensions "E", # pycodestyle errors "EM", # flake8-errmsg "F", # pyflakes errors diff --git a/setup.py b/setup.py index f13f03713..2a364ba97 100755 --- a/setup.py +++ b/setup.py @@ -440,17 +440,17 @@ class pil_build_ext(build_ext): # # add configured kits - for root_name, lib_name in dict( - JPEG_ROOT="libjpeg", - JPEG2K_ROOT="libopenjp2", - TIFF_ROOT=("libtiff-5", "libtiff-4"), - ZLIB_ROOT="zlib", - FREETYPE_ROOT="freetype2", - HARFBUZZ_ROOT="harfbuzz", - FRIBIDI_ROOT="fribidi", - LCMS_ROOT="lcms2", - IMAGEQUANT_ROOT="libimagequant", - ).items(): + for root_name, lib_name in { + "JPEG_ROOT": "libjpeg", + "JPEG2K_ROOT": "libopenjp2", + "TIFF_ROOT": ("libtiff-5", "libtiff-4"), + "ZLIB_ROOT": "zlib", + "FREETYPE_ROOT": "freetype2", + "HARFBUZZ_ROOT": "harfbuzz", + "FRIBIDI_ROOT": "fribidi", + "LCMS_ROOT": "lcms2", + "IMAGEQUANT_ROOT": "libimagequant", + }.items(): root = globals()[root_name] if root is None and root_name in os.environ: diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index ef719e3ec..b51019c66 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -396,7 +396,7 @@ def _save(im, fp, filename, bitmap_header=True): dpi = info.get("dpi", (96, 96)) # 1 meter == 39.3701 inches - ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi)) + ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi) stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3) header = 40 # or 64 for OS/2 version 2 diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index 94efff341..fc0dae44b 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -64,8 +64,6 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): d, e, o, a = self.tile[0] self.tile[0] = d, (0, 0) + self.size, o, a - return - # # -------------------------------------------------------------------- diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 17b1a6082..2309cb406 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -3,110 +3,322 @@ A Pillow loader for .dds files (S3TC-compressed aka DXTC) Jerome Leclanche Documentation: - https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt +https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt The contents of this file are hereby released in the public domain (CC0) Full text of the CC0 license: - https://creativecommons.org/publicdomain/zero/1.0/ +https://creativecommons.org/publicdomain/zero/1.0/ """ +import io import struct -from io import BytesIO +import sys +from enum import IntEnum, IntFlag from . import Image, ImageFile, ImagePalette from ._binary import o8 +from ._binary import i32le as i32 from ._binary import o32le as o32 # Magic ("DDS ") DDS_MAGIC = 0x20534444 + # DDS flags -DDSD_CAPS = 0x1 -DDSD_HEIGHT = 0x2 -DDSD_WIDTH = 0x4 -DDSD_PITCH = 0x8 -DDSD_PIXELFORMAT = 0x1000 -DDSD_MIPMAPCOUNT = 0x20000 -DDSD_LINEARSIZE = 0x80000 -DDSD_DEPTH = 0x800000 +class DDSD(IntFlag): + CAPS = 0x1 + HEIGHT = 0x2 + WIDTH = 0x4 + PITCH = 0x8 + PIXELFORMAT = 0x1000 + MIPMAPCOUNT = 0x20000 + LINEARSIZE = 0x80000 + DEPTH = 0x800000 + # DDS caps -DDSCAPS_COMPLEX = 0x8 -DDSCAPS_TEXTURE = 0x1000 -DDSCAPS_MIPMAP = 0x400000 +class DDSCAPS(IntFlag): + COMPLEX = 0x8 + TEXTURE = 0x1000 + MIPMAP = 0x400000 + + +class DDSCAPS2(IntFlag): + CUBEMAP = 0x200 + CUBEMAP_POSITIVEX = 0x400 + CUBEMAP_NEGATIVEX = 0x800 + CUBEMAP_POSITIVEY = 0x1000 + CUBEMAP_NEGATIVEY = 0x2000 + CUBEMAP_POSITIVEZ = 0x4000 + CUBEMAP_NEGATIVEZ = 0x8000 + VOLUME = 0x200000 -DDSCAPS2_CUBEMAP = 0x200 -DDSCAPS2_CUBEMAP_POSITIVEX = 0x400 -DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800 -DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000 -DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000 -DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000 -DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000 -DDSCAPS2_VOLUME = 0x200000 # Pixel Format -DDPF_ALPHAPIXELS = 0x1 -DDPF_ALPHA = 0x2 -DDPF_FOURCC = 0x4 -DDPF_PALETTEINDEXED8 = 0x20 -DDPF_RGB = 0x40 -DDPF_LUMINANCE = 0x20000 - - -# dds.h - -DDS_FOURCC = DDPF_FOURCC -DDS_RGB = DDPF_RGB -DDS_RGBA = DDPF_RGB | DDPF_ALPHAPIXELS -DDS_LUMINANCE = DDPF_LUMINANCE -DDS_LUMINANCEA = DDPF_LUMINANCE | DDPF_ALPHAPIXELS -DDS_ALPHA = DDPF_ALPHA -DDS_PAL8 = DDPF_PALETTEINDEXED8 - -DDS_HEADER_FLAGS_TEXTURE = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT -DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT -DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH -DDS_HEADER_FLAGS_PITCH = DDSD_PITCH -DDS_HEADER_FLAGS_LINEARSIZE = DDSD_LINEARSIZE - -DDS_HEIGHT = DDSD_HEIGHT -DDS_WIDTH = DDSD_WIDTH - -DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS_TEXTURE -DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS_COMPLEX | DDSCAPS_MIPMAP -DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS_COMPLEX - -DDS_CUBEMAP_POSITIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX -DDS_CUBEMAP_NEGATIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX -DDS_CUBEMAP_POSITIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY -DDS_CUBEMAP_NEGATIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY -DDS_CUBEMAP_POSITIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ -DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ - - -# DXT1 -DXT1_FOURCC = 0x31545844 - -# DXT3 -DXT3_FOURCC = 0x33545844 - -# DXT5 -DXT5_FOURCC = 0x35545844 +class DDPF(IntFlag): + ALPHAPIXELS = 0x1 + ALPHA = 0x2 + FOURCC = 0x4 + PALETTEINDEXED8 = 0x20 + RGB = 0x40 + LUMINANCE = 0x20000 # dxgiformat.h +class DXGI_FORMAT(IntEnum): + UNKNOWN = 0 + R32G32B32A32_TYPELESS = 1 + R32G32B32A32_FLOAT = 2 + R32G32B32A32_UINT = 3 + R32G32B32A32_SINT = 4 + R32G32B32_TYPELESS = 5 + R32G32B32_FLOAT = 6 + R32G32B32_UINT = 7 + R32G32B32_SINT = 8 + R16G16B16A16_TYPELESS = 9 + R16G16B16A16_FLOAT = 10 + R16G16B16A16_UNORM = 11 + R16G16B16A16_UINT = 12 + R16G16B16A16_SNORM = 13 + R16G16B16A16_SINT = 14 + R32G32_TYPELESS = 15 + R32G32_FLOAT = 16 + R32G32_UINT = 17 + R32G32_SINT = 18 + R32G8X24_TYPELESS = 19 + D32_FLOAT_S8X24_UINT = 20 + R32_FLOAT_X8X24_TYPELESS = 21 + X32_TYPELESS_G8X24_UINT = 22 + R10G10B10A2_TYPELESS = 23 + R10G10B10A2_UNORM = 24 + R10G10B10A2_UINT = 25 + R11G11B10_FLOAT = 26 + R8G8B8A8_TYPELESS = 27 + R8G8B8A8_UNORM = 28 + R8G8B8A8_UNORM_SRGB = 29 + R8G8B8A8_UINT = 30 + R8G8B8A8_SNORM = 31 + R8G8B8A8_SINT = 32 + R16G16_TYPELESS = 33 + R16G16_FLOAT = 34 + R16G16_UNORM = 35 + R16G16_UINT = 36 + R16G16_SNORM = 37 + R16G16_SINT = 38 + R32_TYPELESS = 39 + D32_FLOAT = 40 + R32_FLOAT = 41 + R32_UINT = 42 + R32_SINT = 43 + R24G8_TYPELESS = 44 + D24_UNORM_S8_UINT = 45 + R24_UNORM_X8_TYPELESS = 46 + X24_TYPELESS_G8_UINT = 47 + R8G8_TYPELESS = 48 + R8G8_UNORM = 49 + R8G8_UINT = 50 + R8G8_SNORM = 51 + R8G8_SINT = 52 + R16_TYPELESS = 53 + R16_FLOAT = 54 + D16_UNORM = 55 + R16_UNORM = 56 + R16_UINT = 57 + R16_SNORM = 58 + R16_SINT = 59 + R8_TYPELESS = 60 + R8_UNORM = 61 + R8_UINT = 62 + R8_SNORM = 63 + R8_SINT = 64 + A8_UNORM = 65 + R1_UNORM = 66 + R9G9B9E5_SHAREDEXP = 67 + R8G8_B8G8_UNORM = 68 + G8R8_G8B8_UNORM = 69 + BC1_TYPELESS = 70 + BC1_UNORM = 71 + BC1_UNORM_SRGB = 72 + BC2_TYPELESS = 73 + BC2_UNORM = 74 + BC2_UNORM_SRGB = 75 + BC3_TYPELESS = 76 + BC3_UNORM = 77 + BC3_UNORM_SRGB = 78 + BC4_TYPELESS = 79 + BC4_UNORM = 80 + BC4_SNORM = 81 + BC5_TYPELESS = 82 + BC5_UNORM = 83 + BC5_SNORM = 84 + B5G6R5_UNORM = 85 + B5G5R5A1_UNORM = 86 + B8G8R8A8_UNORM = 87 + B8G8R8X8_UNORM = 88 + R10G10B10_XR_BIAS_A2_UNORM = 89 + B8G8R8A8_TYPELESS = 90 + B8G8R8A8_UNORM_SRGB = 91 + B8G8R8X8_TYPELESS = 92 + B8G8R8X8_UNORM_SRGB = 93 + BC6H_TYPELESS = 94 + BC6H_UF16 = 95 + BC6H_SF16 = 96 + BC7_TYPELESS = 97 + BC7_UNORM = 98 + BC7_UNORM_SRGB = 99 + AYUV = 100 + Y410 = 101 + Y416 = 102 + NV12 = 103 + P010 = 104 + P016 = 105 + OPAQUE_420 = 106 + YUY2 = 107 + Y210 = 108 + Y216 = 109 + NV11 = 110 + AI44 = 111 + IA44 = 112 + P8 = 113 + A8P8 = 114 + B4G4R4A4_UNORM = 115 + P208 = 130 + V208 = 131 + V408 = 132 + SAMPLER_FEEDBACK_MIN_MIP_OPAQUE = 189 + SAMPLER_FEEDBACK_MIP_REGION_USED_OPAQUE = 190 -DXGI_FORMAT_R8G8B8A8_TYPELESS = 27 -DXGI_FORMAT_R8G8B8A8_UNORM = 28 -DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 -DXGI_FORMAT_BC5_TYPELESS = 82 -DXGI_FORMAT_BC5_UNORM = 83 -DXGI_FORMAT_BC5_SNORM = 84 -DXGI_FORMAT_BC6H_UF16 = 95 -DXGI_FORMAT_BC6H_SF16 = 96 -DXGI_FORMAT_BC7_TYPELESS = 97 -DXGI_FORMAT_BC7_UNORM = 98 -DXGI_FORMAT_BC7_UNORM_SRGB = 99 + +class D3DFMT(IntEnum): + UNKNOWN = 0 + R8G8B8 = 20 + A8R8G8B8 = 21 + X8R8G8B8 = 22 + R5G6B5 = 23 + X1R5G5B5 = 24 + A1R5G5B5 = 25 + A4R4G4B4 = 26 + R3G3B2 = 27 + A8 = 28 + A8R3G3B2 = 29 + X4R4G4B4 = 30 + A2B10G10R10 = 31 + A8B8G8R8 = 32 + X8B8G8R8 = 33 + G16R16 = 34 + A2R10G10B10 = 35 + A16B16G16R16 = 36 + A8P8 = 40 + P8 = 41 + L8 = 50 + A8L8 = 51 + A4L4 = 52 + V8U8 = 60 + L6V5U5 = 61 + X8L8V8U8 = 62 + Q8W8V8U8 = 63 + V16U16 = 64 + A2W10V10U10 = 67 + D16_LOCKABLE = 70 + D32 = 71 + D15S1 = 73 + D24S8 = 75 + D24X8 = 77 + D24X4S4 = 79 + D16 = 80 + D32F_LOCKABLE = 82 + D24FS8 = 83 + D32_LOCKABLE = 84 + S8_LOCKABLE = 85 + L16 = 81 + VERTEXDATA = 100 + INDEX16 = 101 + INDEX32 = 102 + Q16W16V16U16 = 110 + R16F = 111 + G16R16F = 112 + A16B16G16R16F = 113 + R32F = 114 + G32R32F = 115 + A32B32G32R32F = 116 + CxV8U8 = 117 + A1 = 118 + A2B10G10R10_XR_BIAS = 119 + BINARYBUFFER = 199 + + UYVY = i32(b"UYVY") + R8G8_B8G8 = i32(b"RGBG") + YUY2 = i32(b"YUY2") + G8R8_G8B8 = i32(b"GRGB") + DXT1 = i32(b"DXT1") + DXT2 = i32(b"DXT2") + DXT3 = i32(b"DXT3") + DXT4 = i32(b"DXT4") + DXT5 = i32(b"DXT5") + DX10 = i32(b"DX10") + BC4S = i32(b"BC4S") + BC4U = i32(b"BC4U") + BC5S = i32(b"BC5S") + BC5U = i32(b"BC5U") + ATI1 = i32(b"ATI1") + ATI2 = i32(b"ATI2") + MULTI2_ARGB8 = i32(b"MET1") + + +# Backward compatibility layer +module = sys.modules[__name__] +for item in DDSD: + setattr(module, "DDSD_" + item.name, item.value) +for item in DDSCAPS: + setattr(module, "DDSCAPS_" + item.name, item.value) +for item in DDSCAPS2: + setattr(module, "DDSCAPS2_" + item.name, item.value) +for item in DDPF: + setattr(module, "DDPF_" + item.name, item.value) + +DDS_FOURCC = DDPF.FOURCC +DDS_RGB = DDPF.RGB +DDS_RGBA = DDPF.RGB | DDPF.ALPHAPIXELS +DDS_LUMINANCE = DDPF.LUMINANCE +DDS_LUMINANCEA = DDPF.LUMINANCE | DDPF.ALPHAPIXELS +DDS_ALPHA = DDPF.ALPHA +DDS_PAL8 = DDPF.PALETTEINDEXED8 + +DDS_HEADER_FLAGS_TEXTURE = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PIXELFORMAT +DDS_HEADER_FLAGS_MIPMAP = DDSD.MIPMAPCOUNT +DDS_HEADER_FLAGS_VOLUME = DDSD.DEPTH +DDS_HEADER_FLAGS_PITCH = DDSD.PITCH +DDS_HEADER_FLAGS_LINEARSIZE = DDSD.LINEARSIZE + +DDS_HEIGHT = DDSD.HEIGHT +DDS_WIDTH = DDSD.WIDTH + +DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS.TEXTURE +DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS.COMPLEX | DDSCAPS.MIPMAP +DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS.COMPLEX + +DDS_CUBEMAP_POSITIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEX +DDS_CUBEMAP_NEGATIVEX = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEX +DDS_CUBEMAP_POSITIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEY +DDS_CUBEMAP_NEGATIVEY = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEY +DDS_CUBEMAP_POSITIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_POSITIVEZ +DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2.CUBEMAP | DDSCAPS2.CUBEMAP_NEGATIVEZ + +DXT1_FOURCC = D3DFMT.DXT1 +DXT3_FOURCC = D3DFMT.DXT3 +DXT5_FOURCC = D3DFMT.DXT5 + +DXGI_FORMAT_R8G8B8A8_TYPELESS = DXGI_FORMAT.R8G8B8A8_TYPELESS +DXGI_FORMAT_R8G8B8A8_UNORM = DXGI_FORMAT.R8G8B8A8_UNORM +DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = DXGI_FORMAT.R8G8B8A8_UNORM_SRGB +DXGI_FORMAT_BC5_TYPELESS = DXGI_FORMAT.BC5_TYPELESS +DXGI_FORMAT_BC5_UNORM = DXGI_FORMAT.BC5_UNORM +DXGI_FORMAT_BC5_SNORM = DXGI_FORMAT.BC5_SNORM +DXGI_FORMAT_BC6H_UF16 = DXGI_FORMAT.BC6H_UF16 +DXGI_FORMAT_BC6H_SF16 = DXGI_FORMAT.BC6H_SF16 +DXGI_FORMAT_BC7_TYPELESS = DXGI_FORMAT.BC7_TYPELESS +DXGI_FORMAT_BC7_UNORM = DXGI_FORMAT.BC7_UNORM +DXGI_FORMAT_BC7_UNORM_SRGB = DXGI_FORMAT.BC7_UNORM_SRGB class DdsImageFile(ImageFile.ImageFile): @@ -125,112 +337,134 @@ class DdsImageFile(ImageFile.ImageFile): if len(header_bytes) != 120: msg = f"Incomplete header: {len(header_bytes)} bytes" raise OSError(msg) - header = BytesIO(header_bytes) + header = io.BytesIO(header_bytes) flags, height, width = struct.unpack("<3I", header.read(12)) self._size = (width, height) - self._mode = "RGBA" + extents = (0, 0) + self.size pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) struct.unpack("<11I", header.read(44)) # reserved # pixel format - pfsize, pfflags = struct.unpack("<2I", header.read(8)) - fourcc = header.read(4) - (bitcount,) = struct.unpack(" 4: msg = "Invalid number of bands" raise OSError(msg) - for i in range(bands): - # note: for now, we ignore the "uncalibrated" flag - colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF) - self._mode, self.rawmode = MODES[tuple(colors)] + # note: for now, we ignore the "uncalibrated" flag + colors = tuple(i32(s, 8 + i * 4) & 0x7FFFFFFF for i in range(bands)) + + self._mode, self.rawmode = MODES[colors] # load JPEG tables, if any self.jpeg = {} @@ -227,6 +226,7 @@ class FpxImageFile(ImageFile.ImageFile): break # isn't really required self.stream = stream + self._fp = self.fp self.fp = None def load(self): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d435b5617..8c17292a7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -40,7 +40,7 @@ from enum import IntEnum from pathlib import Path try: - import defusedxml.ElementTree as ElementTree + from defusedxml import ElementTree except ImportError: ElementTree = None @@ -1160,7 +1160,7 @@ class Image: if palette.mode != "P": msg = "bad mode for palette image" raise ValueError(msg) - if self.mode != "RGB" and self.mode != "L": + if self.mode not in {"RGB", "L"}: msg = "only RGB or L mode images can be quantized to a palette" raise ValueError(msg) im = self.im.convert("P", dither, palette.im) @@ -1288,9 +1288,9 @@ class Image: if self.im.bands == 1 or multiband: return self._new(filter.filter(self.im)) - ims = [] - for c in range(self.im.bands): - ims.append(self._new(filter.filter(self.im.getband(c)))) + ims = [ + self._new(filter.filter(self.im.getband(c))) for c in range(self.im.bands) + ] return merge(self.mode, ims) def getbands(self): @@ -1339,10 +1339,7 @@ class Image: self.load() if self.mode in ("1", "L", "P"): h = self.im.histogram() - out = [] - for i in range(256): - if h[i]: - out.append((h[i], i)) + out = [(h[i], i) for i in range(256) if h[i]] if len(out) > maxcolors: return None return out @@ -1383,10 +1380,7 @@ class Image: self.load() if self.im.bands > 1: - extrema = [] - for i in range(self.im.bands): - extrema.append(self.im.getband(i).getextrema()) - return tuple(extrema) + return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) return self.im.getextrema() def _getxmp(self, xmp_tags): diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 3a337f9f2..0df3a4c6c 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -787,11 +787,8 @@ def getProfileInfo(profile): # info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint description = profile.profile.profile_description cpright = profile.profile.copyright - arr = [] - for elt in (description, cpright): - if elt: - arr.append(elt) - return "\r\n\r\n".join(arr) + "\r\n\r\n" + elements = [element for element in (description, cpright) if element] + return "\r\n\r\n".join(elements) + "\r\n\r\n" except (AttributeError, OSError, TypeError, ValueError) as v: raise PyCMSError(v) from v diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index fbf320d72..6509d4c8e 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -921,7 +921,7 @@ def floodfill(image, xy, value, border=None, thresh=0): if border is None: fill = _color_diff(p, background) <= thresh else: - fill = p != value and p != border + fill = p not in (value, border) if fill: pixel[s, t] = value new_edge.add((s, t)) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 902e8ce5f..8bca19a4b 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -26,11 +26,13 @@ # # See the README file for information on usage and redistribution. # +from __future__ import annotations import io import itertools import struct import sys +from typing import NamedTuple from . import Image from ._util import is_path @@ -77,6 +79,13 @@ def _tilesort(t): return t[2] +class _Tile(NamedTuple): + encoder_name: str + extents: tuple[int, int, int, int] + offset: int + args: tuple | str | None + + # # -------------------------------------------------------------------- # ImageFile base class @@ -520,13 +529,13 @@ def _save(im, fp, tile, bufsize=0): fp.flush() -def _encode_tile(im, fp, tile, bufsize, fh, exc=None): - for e, b, o, a in tile: - if o > 0: - fp.seek(o) - encoder = Image._getencoder(im.mode, e, a, im.encoderconfig) +def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None): + for encoder_name, extents, offset, args in tile: + if offset > 0: + fp.seek(offset) + encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig) try: - encoder.setimage(im.im, b) + encoder.setimage(im.im, extents) if encoder.pushes_fd: encoder.setfd(fp) errcode = encoder.encode_to_pyfd()[1] diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 0331a5c45..f406bfc94 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -25,12 +25,16 @@ # See the README file for information on usage and redistribution. # +from __future__ import annotations + import base64 import os import sys import warnings from enum import IntEnum from io import BytesIO +from pathlib import Path +from typing import IO from . import Image from ._util import is_directory, is_path @@ -185,9 +189,20 @@ class ImageFont: class FreeTypeFont: """FreeType font wrapper (requires _imagingft service)""" - def __init__(self, font=None, size=10, index=0, encoding="", layout_engine=None): + def __init__( + self, + font: bytes | str | Path | IO | None = None, + size: float = 10, + index: int = 0, + encoding: str = "", + layout_engine: Layout | None = None, + ) -> None: # FIXME: use service provider instead + if size <= 0: + msg = "font size must be greater than 0" + raise ValueError(msg) + self.path = font self.size = size self.index = index @@ -213,6 +228,8 @@ class FreeTypeFont: ) if is_path(font): + if isinstance(font, Path): + font = str(font) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: @@ -775,7 +792,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): This specifies the character set to use. It does not alter the encoding of any text provided in subsequent operations. :param layout_engine: Which layout engine to use, if available: - :data:`.ImageFont.Layout.BASIC` or :data:`.ImageFont.Layout.RAQM`. + :attr:`.ImageFont.Layout.BASIC` or :attr:`.ImageFont.Layout.RAQM`. If it is available, Raqm layout will be used by default. Otherwise, basic layout will be used. @@ -791,10 +808,6 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): :exception ValueError: If the font size is not greater than zero. """ - if size <= 0: - msg = "font size must be greater than 0" - raise ValueError(msg) - def freetype(font): return FreeTypeFont(font, size, index, encoding, layout_engine) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 4f83a4edb..f183c8f27 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -557,9 +557,7 @@ def invert(image): :param image: The image to invert. :return: An image. """ - lut = [] - for i in range(256): - lut.append(255 - i) + lut = list(range(255, -1, -1)) return image.point(lut) if image.mode == "1" else _lut(image, lut) @@ -581,10 +579,8 @@ def posterize(image, bits): :param bits: The number of bits to keep for each channel (1-8). :return: An image. """ - lut = [] mask = ~(2 ** (8 - bits) - 1) - for i in range(256): - lut.append(i & mask) + lut = [i & mask for i in range(256)] return _lut(image, lut) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index cb4f1dba1..f9295e299 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -200,21 +200,15 @@ def raw(rawmode, data): def make_linear_lut(black, white): - lut = [] if black == 0: - for i in range(256): - lut.append(white * i // 255) - else: - msg = "unavailable when black is non-zero" - raise NotImplementedError(msg) # FIXME - return lut + return [white * i // 255 for i in range(256)] + + msg = "unavailable when black is non-zero" + raise NotImplementedError(msg) # FIXME def make_gamma_lut(exp): - lut = [] - for i in range(256): - lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5)) - return lut + return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)] def negative(mode="RGB"): @@ -226,9 +220,7 @@ def negative(mode="RGB"): def random(mode="RGB"): from random import randint - palette = [] - for i in range(256 * len(mode)): - palette.append(randint(0, 255)) + palette = [randint(0, 255) for _ in range(256 * len(mode))] return ImagePalette(mode, palette) diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index d017565a9..56c1aa525 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -103,12 +103,10 @@ def align8to32(bytes, width, mode): if not extra_padding: return bytes - new_data = [] - for i in range(len(bytes) // bytes_per_line): - new_data.append( - bytes[i * bytes_per_line : (i + 1) * bytes_per_line] - + b"\x00" * extra_padding - ) + new_data = [ + bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + b"\x00" * extra_padding + for i in range(len(bytes) // bytes_per_line) + ] return b"".join(new_data) @@ -131,15 +129,11 @@ def _toqclass_helper(im): format = qt_format.Format_Mono elif im.mode == "L": format = qt_format.Format_Indexed8 - colortable = [] - for i in range(256): - colortable.append(rgb(i, i, i)) + colortable = [rgb(i, i, i) for i in range(256)] elif im.mode == "P": format = qt_format.Format_Indexed8 - colortable = [] palette = im.getpalette() - for i in range(0, len(palette), 3): - colortable.append(rgb(*palette[i : i + 3])) + colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)] elif im.mode == "RGB": # Populate the 4th channel with 255 im = im.convert("RGBA") diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index b7ebddf06..edc39fb53 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -21,9 +21,7 @@ # See the README file for information on usage and redistribution. # -import functools import math -import operator class Stat: @@ -53,26 +51,22 @@ class Stat: """Get min/max values for each band in the image""" def minmax(histogram): - n = 255 - x = 0 + res_min, res_max = 255, 0 for i in range(256): if histogram[i]: - n = min(n, i) - x = max(x, i) - return n, x # returns (255, 0) if there's no data in the histogram + res_min = i + break + for i in range(255, -1, -1): + if histogram[i]: + res_max = i + break + return res_min, res_max - v = [] - for i in range(0, len(self.h), 256): - v.append(minmax(self.h[i:])) - return v + return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)] def _getcount(self): """Get total number of pixels in each layer""" - - v = [] - for i in range(0, len(self.h), 256): - v.append(functools.reduce(operator.add, self.h[i : i + 256])) - return v + return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)] def _getsum(self): """Get sum of all pixels in each layer""" @@ -98,11 +92,7 @@ class Stat: def _getmean(self): """Get average pixel level for each layer""" - - v = [] - for i in self.bands: - v.append(self.sum[i] / self.count[i]) - return v + return [self.sum[i] / self.count[i] for i in self.bands] def _getmedian(self): """Get median pixel level for each layer""" @@ -121,28 +111,18 @@ class Stat: def _getrms(self): """Get RMS for each layer""" - - v = [] - for i in self.bands: - v.append(math.sqrt(self.sum2[i] / self.count[i])) - return v + return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands] def _getvar(self): """Get variance for each layer""" - - v = [] - for i in self.bands: - n = self.count[i] - v.append((self.sum2[i] - (self.sum[i] ** 2.0) / n) / n) - return v + return [ + (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i] + for i in self.bands + ] def _getstddev(self): """Get standard deviation for each layer""" - - v = [] - for i in self.bands: - v.append(math.sqrt(self.var[i])) - return v + return [math.sqrt(self.var[i]) for i in self.bands] Global = Stat # compatibility diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 963d6c1a3..bb0cb676a 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -334,10 +334,7 @@ def _save(im, fp, filename): if quality_layers is not None and not ( isinstance(quality_layers, (list, tuple)) and all( - [ - isinstance(quality_layer, (int, float)) - for quality_layer in quality_layers - ] + isinstance(quality_layer, (int, float)) for quality_layer in quality_layers ) ): msg = "quality_layers must be a sequence of numbers" diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 3596e0899..5add65f45 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -233,9 +233,7 @@ def SOF(self, marker): # fixup icc profile self.icclist.sort() # sort by sequence number if self.icclist[0][13] == len(self.icclist): - profile = [] - for p in self.icclist: - profile.append(p[14:]) + profile = [p[14:] for p in self.icclist] icc_profile = b"".join(profile) else: icc_profile = None # wrong number of fragments @@ -397,7 +395,7 @@ class JpegImageFile(ImageFile.ImageFile): # self.__offset = self.fp.tell() break s = self.fp.read(1) - elif i == 0 or i == 0xFFFF: + elif i in {0, 0xFFFF}: # padded marker or junk; move on s = b"\xff" elif i == 0xFF00: # Skip extraneous data (escaped 0xFF) diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 801318930..9300d3545 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -51,10 +51,11 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): # find ACI subfiles with Image members (maybe not the # best way to identify MIC files, but what the... ;-) - self.images = [] - for path in self.ole.listdir(): - if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image": - self.images.append(path) + self.images = [ + path + for path in self.ole.listdir() + if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image" + ] # if we didn't find any images, this is probably not # an MIC file. @@ -66,6 +67,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): self._n_frames = len(self.images) self.is_animated = self._n_frames > 1 + self.__fp = self.fp self.seek(0) def seek(self, frame): @@ -87,10 +89,12 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): return self.frame def close(self): + self.__fp.close() self.ole.close() super().close() def __exit__(self, *args): + self.__fp.close() self.ole.close() super().__exit__() diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 8db5822fe..8b0014f3a 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -129,9 +129,8 @@ class PcfFontFile(FontFile.FontFile): nprops = i32(fp.read(4)) # read property description - p = [] - for i in range(nprops): - p.append((i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4)))) + p = [(i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))) for _ in range(nprops)] + if nprops & 3: fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad @@ -186,8 +185,6 @@ class PcfFontFile(FontFile.FontFile): # # bitmap data - bitmaps = [] - fp, format, i16, i32 = self._getformat(PCF_BITMAPS) nbitmaps = i32(fp.read(4)) @@ -196,13 +193,9 @@ class PcfFontFile(FontFile.FontFile): msg = "Wrong number of bitmaps" raise OSError(msg) - offsets = [] - for i in range(nbitmaps): - offsets.append(i32(fp.read(4))) + offsets = [i32(fp.read(4)) for _ in range(nbitmaps)] - bitmap_sizes = [] - for i in range(4): - bitmap_sizes.append(i32(fp.read(4))) + bitmap_sizes = [i32(fp.read(4)) for _ in range(4)] # byteorder = format & 4 # non-zero => MSB bitorder = format & 8 # non-zero => MSB @@ -218,6 +211,7 @@ class PcfFontFile(FontFile.FontFile): if bitorder: mode = "1" + bitmaps = [] for i in range(nbitmaps): xsize, ysize = metrics[i][:2] b, e = offsets[i : i + 2] diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 09fc0c7e6..b6bb60911 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -96,7 +96,7 @@ def _write_image(im, filename, existing_pdf, image_refs): dict_obj["ColorSpace"] = [ PdfParser.PdfName("Indexed"), PdfParser.PdfName("DeviceRGB"), - 255, + len(palette) // 3 - 1, PdfParser.PdfBinary(palette), ] procset = "ImageI" # indexed color diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index acb9ce5a3..a2a259c89 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -123,7 +123,7 @@ class SgiImageFile(ImageFile.ImageFile): def _save(im, fp, filename): - if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L": + if im.mode not in {"RGB", "RGBA", "L"}: msg = "Unsupported SGI image mode" raise ValueError(msg) @@ -155,7 +155,7 @@ def _save(im, fp, filename): # Z Dimension: Number of channels z = len(im.mode) - if dim == 1 or dim == 2: + if dim in {1, 2}: z = 1 # assert we've got the right number of bands. diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 408b982b5..14cad8f9a 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -238,9 +238,7 @@ def makeSpiderHeader(im): if nvalues < 23: return [] - hdr = [] - for i in range(nvalues): - hdr.append(0.0) + hdr = [0.0] * nvalues # NB these are Fortran indices hdr[1] = 1.0 # nslice (=1 for an image) diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index edcae41e4..b02c637b6 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -427,7 +427,7 @@ def _populate(): TAGS_V2[k] = TagInfo(k, *v) - for group, tags in TAGS_V2_GROUPS.items(): + for tags in TAGS_V2_GROUPS.values(): for k, v in tags.items(): tags[k] = TagInfo(k, *v) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 6b593d499..f7e145fb9 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -279,10 +279,10 @@ DEPS = { "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], }, "lcms2": { - "url": SF_PROJECTS + "/lcms/files/lcms/2.15/lcms2-2.15.tar.gz/download", - "filename": "lcms2-2.15.tar.gz", - "dir": "lcms2-2.15", - "license": "COPYING", + "url": SF_PROJECTS + "/lcms/files/lcms/2.16/lcms2-2.16.tar.gz/download", + "filename": "lcms2-2.16.tar.gz", + "dir": "lcms2-2.16", + "license": "LICENSE", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { # default is /MD for x86 and /MT for x64, we need /MD always @@ -345,9 +345,9 @@ DEPS = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/8.2.1.zip", - "filename": "harfbuzz-8.2.1.zip", - "dir": "harfbuzz-8.2.1", + "url": "https://github.com/harfbuzz/harfbuzz/archive/8.3.0.zip", + "filename": "harfbuzz-8.3.0.zip", + "dir": "harfbuzz-8.3.0", "license": "COPYING", "build": [ *cmds_cmake( @@ -482,7 +482,7 @@ def extract_dep(url: str, filename: str) -> None: msg = "Attempted Path Traversal in Zip File" raise RuntimeError(msg) zf.extractall(sources_dir) - elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): + elif filename.endswith((".tar.gz", ".tgz")): with tarfile.open(file, "r:gz") as tgz: for member in tgz.getnames(): member_abspath = os.path.abspath(os.path.join(sources_dir, member)) @@ -586,14 +586,19 @@ def build_dep(name: str) -> str: def build_dep_all() -> None: lines = [r'call "{build_dir}\build_env.cmd"'] + gha_groups = "GITHUB_ACTIONS" in os.environ for dep_name in DEPS: print() if dep_name in disabled: print(f"Skipping disabled dependency {dep_name}") continue script = build_dep(dep_name) + if gha_groups: + lines.append(f"@echo ::group::Running {script}") lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') lines.append("if errorlevel 1 echo Build failed! && exit /B 1") + if gha_groups: + lines.append("@echo ::endgroup::") print() lines.append("@echo All Pillow dependencies built successfully!") write_script("build_dep_all.cmd", lines)