Merge branch 'main' into dds_rgb

This commit is contained in:
Andrew Murray 2023-12-07 08:43:57 +11:00
commit 8b44116773
54 changed files with 909 additions and 449 deletions

View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.4 rev: v0.1.6
hooks: hooks:
- id: ruff - id: ruff
args: [--fix, --exit-non-zero-on-fix] args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.10.1 rev: 23.11.0
hooks: hooks:
- id: black - id: black
@ -42,12 +42,12 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.8.1 rev: v0.9.0
hooks: hooks:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: 1.4.1 rev: 1.5.3
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt

View File

@ -3,7 +3,6 @@ if: tag IS present OR type = api
env: env:
global: global:
- CIBW_ARCHS=aarch64 - CIBW_ARCHS=aarch64
- CIBW_CONFIG_SETTINGS="raqm=enable raqm=vendor fribidi=vendor"
- CIBW_SKIP=pp38-* - CIBW_SKIP=pp38-*
language: python language: python

View File

@ -5,7 +5,25 @@ Changelog (Pillow)
10.2.0 (unreleased) 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] [akx, radarhere]
- If absent, do not try to close fp when closing image #7557 - If absent, do not try to close fp when closing image #7557

View File

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

41
Tests/check_wheel.py Normal file
View File

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

View File

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

BIN
Tests/images/bc1.dds Executable file

Binary file not shown.

BIN
Tests/images/bc1_typeless.dds Executable file

Binary file not shown.

Binary file not shown.

BIN
Tests/images/bc4_unorm.dds Normal file

Binary file not shown.

BIN
Tests/images/bc4_unorm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

BIN
Tests/images/bc4u.dds Normal file

Binary file not shown.

Binary file not shown.

View File

@ -356,9 +356,7 @@ def test_apng_save(tmp_path):
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/single_frame_default.png") as im: with Image.open("Tests/images/apng/single_frame_default.png") as im:
frames = [] frames = [frame_im.copy() for frame_im in ImageSequence.Iterator(im)]
for frame_im in ImageSequence.Iterator(im):
frames.append(frame_im.copy())
frames[0].save( frames[0].save(
test_file, save_all=True, default_image=True, append_images=frames[1:] test_file, save_all=True, default_image=True, append_images=frames[1:]
) )

View File

@ -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_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
TEST_FILE_ATI1 = "Tests/images/ati1.dds" TEST_FILE_ATI1 = "Tests/images/ati1.dds"
TEST_FILE_ATI2 = "Tests/images/ati2.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_TYPELESS = "Tests/images/bc5_typeless.dds"
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.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_BC5S = "Tests/images/bc5s.dds"
TEST_FILE_BC5U = "Tests/images/bc5u.dds" TEST_FILE_BC5U = "Tests/images/bc5u.dds"
TEST_FILE_BC6H = "Tests/images/bc6h.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" TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
def test_sanity_dxt1(): @pytest.mark.parametrize(
"""Check DXT1 images can be opened""" "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: with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
target = target.convert("RGBA") target = target.convert("RGBA")
with Image.open(TEST_FILE_DXT1) as im: with Image.open(image_path) as im:
im.load() im.load()
assert im.format == "DDS" assert im.format == "DDS"
@ -70,10 +84,18 @@ def test_sanity_dxt5():
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png")) assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
def test_sanity_ati1(): @pytest.mark.parametrize(
"""Check ATI1 images can be opened""" "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() im.load()
assert im.format == "DDS" assert im.format == "DDS"
@ -83,6 +105,27 @@ def test_sanity_ati1():
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) 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( @pytest.mark.parametrize(
"image_path", "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( @pytest.mark.parametrize(
("mode", "size", "test_file"), ("mode", "size", "test_file"),
[ [
@ -305,9 +342,22 @@ def test_palette():
assert_image_equal_tofile(im, "Tests/images/transparent.gif") 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 pytest.raises(NotImplementedError):
with Image.open("Tests/images/unimplemented_pixel_format.dds"): with Image.open(test_file):
pass pass

View File

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

View File

@ -4,6 +4,7 @@ import re
import shutil import shutil
import sys import sys
from io import BytesIO from io import BytesIO
from pathlib import Path
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
@ -76,8 +77,9 @@ def _render(font, layout_engine):
return img return img
def test_font_with_name(layout_engine): @pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH)))
_render(FONT_PATH, layout_engine) def test_font_with_name(layout_engine, font):
_render(font, layout_engine)
def test_font_with_filelike(layout_engine): def test_font_with_filelike(layout_engine):

View File

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

View File

@ -521,6 +521,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
.. versionadded:: 2.5.0 .. 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** **comment**
A comment about the image. A comment about the image.

View File

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

View File

@ -70,21 +70,20 @@ Methods
Constants Constants
--------- ---------
.. data:: PIL.ImageFont.Layout.BASIC .. class:: Layout
Use basic text layout for TrueType font. .. py:attribute:: BASIC
Advanced features such as text direction are not supported.
.. 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. .. py:attribute:: RAQM
Advanced features are supported.
Requires Raqm, you can check support using Use Raqm text layout for TrueType font.
:py:func:`PIL.features.check_feature` with ``feature="raqm"``. 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 .. data:: MAX_STRING_LENGTH

View File

@ -33,6 +33,14 @@ Plugin reference
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`~PIL.DdsImagePlugin` Module
---------------------------------
.. automodule:: PIL.DdsImagePlugin
:members:
:undoc-members:
:show-inheritance:
:mod:`~PIL.EpsImagePlugin` Module :mod:`~PIL.EpsImagePlugin` Module
--------------------------------- ---------------------------------

View File

@ -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.

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
10.2.0
10.1.0 10.1.0
10.0.1 10.0.1
10.0.0 10.0.0

View File

@ -88,12 +88,15 @@ version = {attr = "PIL.__version__"}
[tool.cibuildwheel] [tool.cibuildwheel]
before-all = ".github/workflows/wheels-dependencies.sh" before-all = ".github/workflows/wheels-dependencies.sh"
build-verbosity = 1
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
test-command = "cd {project} && .github/workflows/wheels-test.sh" test-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests" test-extras = "tests"
[tool.ruff] [tool.ruff]
line-length = 88 line-length = 88
select = [ select = [
"C4", # flake8-comprehensions
"E", # pycodestyle errors "E", # pycodestyle errors
"EM", # flake8-errmsg "EM", # flake8-errmsg
"F", # pyflakes errors "F", # pyflakes errors

View File

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

View File

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

View File

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

View File

@ -3,110 +3,322 @@ A Pillow loader for .dds files (S3TC-compressed aka DXTC)
Jerome Leclanche <jerome@leclan.ch> Jerome Leclanche <jerome@leclan.ch>
Documentation: 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) The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license: Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/ https://creativecommons.org/publicdomain/zero/1.0/
""" """
import io
import struct import struct
from io import BytesIO import sys
from enum import IntEnum, IntFlag
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._binary import o8 from ._binary import o8
from ._binary import i32le as i32
from ._binary import o32le as o32 from ._binary import o32le as o32
# Magic ("DDS ") # Magic ("DDS ")
DDS_MAGIC = 0x20534444 DDS_MAGIC = 0x20534444
# DDS flags # DDS flags
DDSD_CAPS = 0x1 class DDSD(IntFlag):
DDSD_HEIGHT = 0x2 CAPS = 0x1
DDSD_WIDTH = 0x4 HEIGHT = 0x2
DDSD_PITCH = 0x8 WIDTH = 0x4
DDSD_PIXELFORMAT = 0x1000 PITCH = 0x8
DDSD_MIPMAPCOUNT = 0x20000 PIXELFORMAT = 0x1000
DDSD_LINEARSIZE = 0x80000 MIPMAPCOUNT = 0x20000
DDSD_DEPTH = 0x800000 LINEARSIZE = 0x80000
DEPTH = 0x800000
# DDS caps # DDS caps
DDSCAPS_COMPLEX = 0x8 class DDSCAPS(IntFlag):
DDSCAPS_TEXTURE = 0x1000 COMPLEX = 0x8
DDSCAPS_MIPMAP = 0x400000 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 # Pixel Format
DDPF_ALPHAPIXELS = 0x1 class DDPF(IntFlag):
DDPF_ALPHA = 0x2 ALPHAPIXELS = 0x1
DDPF_FOURCC = 0x4 ALPHA = 0x2
DDPF_PALETTEINDEXED8 = 0x20 FOURCC = 0x4
DDPF_RGB = 0x40 PALETTEINDEXED8 = 0x20
DDPF_LUMINANCE = 0x20000 RGB = 0x40
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
# dxgiformat.h # 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 class D3DFMT(IntEnum):
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 UNKNOWN = 0
DXGI_FORMAT_BC5_TYPELESS = 82 R8G8B8 = 20
DXGI_FORMAT_BC5_UNORM = 83 A8R8G8B8 = 21
DXGI_FORMAT_BC5_SNORM = 84 X8R8G8B8 = 22
DXGI_FORMAT_BC6H_UF16 = 95 R5G6B5 = 23
DXGI_FORMAT_BC6H_SF16 = 96 X1R5G5B5 = 24
DXGI_FORMAT_BC7_TYPELESS = 97 A1R5G5B5 = 25
DXGI_FORMAT_BC7_UNORM = 98 A4R4G4B4 = 26
DXGI_FORMAT_BC7_UNORM_SRGB = 99 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): class DdsImageFile(ImageFile.ImageFile):
@ -125,112 +337,134 @@ class DdsImageFile(ImageFile.ImageFile):
if len(header_bytes) != 120: if len(header_bytes) != 120:
msg = f"Incomplete header: {len(header_bytes)} bytes" msg = f"Incomplete header: {len(header_bytes)} bytes"
raise OSError(msg) raise OSError(msg)
header = BytesIO(header_bytes) header = io.BytesIO(header_bytes)
flags, height, width = struct.unpack("<3I", header.read(12)) flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height) self._size = (width, height)
self._mode = "RGBA" extents = (0, 0) + self.size
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved struct.unpack("<11I", header.read(44)) # reserved
# pixel format # pixel format
pfsize, pfflags = struct.unpack("<2I", header.read(8)) pfsize, pfflags, fourcc, bitcount = struct.unpack("<4I", header.read(16))
fourcc = header.read(4) n = 0
(bitcount,) = struct.unpack("<I", header.read(4)) rawmode = None
if pfflags & DDPF_LUMINANCE: if pfflags & DDPF.RGB:
# Texture contains uncompressed L or LA data
if pfflags & DDPF_ALPHAPIXELS:
self._mode = "LA"
else:
self._mode = "L"
self.tile = [("raw", (0, 0) + self.size, 0, (self.mode, 0, 1))]
elif pfflags & DDPF_RGB:
# Texture contains uncompressed RGB data # Texture contains uncompressed RGB data
if pfflags & DDPF_ALPHAPIXELS: if pfflags & DDPF.ALPHAPIXELS:
self._mode = "RGBA"
mask_count = 4 mask_count = 4
else: else:
self._mode = "RGB" self._mode = "RGB"
mask_count = 3 mask_count = 3
masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4)) masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4))
self.tile = [("dds_rgb", (0, 0) + self.size, 0, (bitcount, masks))] self.tile = [("dds_rgb", extents, 0, (bitcount, masks))]
elif pfflags & DDPF_PALETTEINDEXED8: return
elif pfflags & DDPF.LUMINANCE:
if bitcount == 8:
self._mode = "L"
elif bitcount == 16 and pfflags & DDPF.ALPHAPIXELS:
self._mode = "LA"
else:
msg = f"Unsupported bitcount {bitcount} for {pfflags}"
raise OSError(msg)
elif pfflags & DDPF.PALETTEINDEXED8:
self._mode = "P" self._mode = "P"
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024)) self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
self.tile = [("raw", (0, 0) + self.size, 0, "L")] elif pfflags & DDPF.FOURCC:
else: offset = header_size + 4
data_start = header_size + 4 if fourcc == D3DFMT.DXT1:
n = 0 self._mode = "RGBA"
if fourcc == b"DXT1":
self.pixel_format = "DXT1" self.pixel_format = "DXT1"
n = 1 n = 1
elif fourcc == b"DXT3": elif fourcc == D3DFMT.DXT3:
self._mode = "RGBA"
self.pixel_format = "DXT3" self.pixel_format = "DXT3"
n = 2 n = 2
elif fourcc == b"DXT5": elif fourcc == D3DFMT.DXT5:
self._mode = "RGBA"
self.pixel_format = "DXT5" self.pixel_format = "DXT5"
n = 3 n = 3
elif fourcc == b"ATI1": elif fourcc in (D3DFMT.BC4U, D3DFMT.ATI1):
self._mode = "L"
self.pixel_format = "BC4" self.pixel_format = "BC4"
n = 4 n = 4
self._mode = "L" elif fourcc == D3DFMT.BC5S:
elif fourcc in (b"ATI2", b"BC5U"):
self.pixel_format = "BC5"
n = 5
self._mode = "RGB" self._mode = "RGB"
elif fourcc == b"BC5S":
self.pixel_format = "BC5S" self.pixel_format = "BC5S"
n = 5 n = 5
elif fourcc in (D3DFMT.BC5U, D3DFMT.ATI2):
self._mode = "RGB" self._mode = "RGB"
elif fourcc == b"DX10": self.pixel_format = "BC5"
data_start += 20 n = 5
elif fourcc == D3DFMT.DX10:
offset += 20
# ignoring flags which pertain to volume textures and cubemaps # ignoring flags which pertain to volume textures and cubemaps
(dxgi_format,) = struct.unpack("<I", self.fp.read(4)) (dxgi_format,) = struct.unpack("<I", self.fp.read(4))
self.fp.read(16) self.fp.read(16)
if dxgi_format in (DXGI_FORMAT_BC5_TYPELESS, DXGI_FORMAT_BC5_UNORM): if dxgi_format in (
DXGI_FORMAT.BC1_UNORM,
DXGI_FORMAT.BC1_TYPELESS,
):
self._mode = "RGBA"
self.pixel_format = "BC1"
n = 1
elif dxgi_format in (DXGI_FORMAT.BC4_TYPELESS, DXGI_FORMAT.BC4_UNORM):
self._mode = "L"
self.pixel_format = "BC4"
n = 4
elif dxgi_format in (DXGI_FORMAT.BC5_TYPELESS, DXGI_FORMAT.BC5_UNORM):
self._mode = "RGB"
self.pixel_format = "BC5" self.pixel_format = "BC5"
n = 5 n = 5
elif dxgi_format == DXGI_FORMAT.BC5_SNORM:
self._mode = "RGB" self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC5_SNORM:
self.pixel_format = "BC5S" self.pixel_format = "BC5S"
n = 5 n = 5
elif dxgi_format == DXGI_FORMAT.BC6H_UF16:
self._mode = "RGB" self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_UF16:
self.pixel_format = "BC6H" self.pixel_format = "BC6H"
n = 6 n = 6
elif dxgi_format == DXGI_FORMAT.BC6H_SF16:
self._mode = "RGB" self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_SF16:
self.pixel_format = "BC6HS" self.pixel_format = "BC6HS"
n = 6 n = 6
self._mode = "RGB"
elif dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM):
self.pixel_format = "BC7"
n = 7
elif dxgi_format == DXGI_FORMAT_BC7_UNORM_SRGB:
self.pixel_format = "BC7"
self.info["gamma"] = 1 / 2.2
n = 7
elif dxgi_format in ( elif dxgi_format in (
DXGI_FORMAT_R8G8B8A8_TYPELESS, DXGI_FORMAT.BC7_TYPELESS,
DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT.BC7_UNORM,
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, DXGI_FORMAT.BC7_UNORM_SRGB,
): ):
self.tile = [("raw", (0, 0) + self.size, 0, ("RGBA", 0, 1))] self._mode = "RGBA"
if dxgi_format == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: self.pixel_format = "BC7"
n = 7
if dxgi_format == DXGI_FORMAT.BC7_UNORM_SRGB:
self.info["gamma"] = 1 / 2.2
elif dxgi_format in (
DXGI_FORMAT.R8G8B8A8_TYPELESS,
DXGI_FORMAT.R8G8B8A8_UNORM,
DXGI_FORMAT.R8G8B8A8_UNORM_SRGB,
):
self._mode = "RGBA"
if dxgi_format == DXGI_FORMAT.R8G8B8A8_UNORM_SRGB:
self.info["gamma"] = 1 / 2.2 self.info["gamma"] = 1 / 2.2
return
else: else:
msg = f"Unimplemented DXGI format {dxgi_format}" msg = f"Unimplemented DXGI format {dxgi_format}"
raise NotImplementedError(msg) raise NotImplementedError(msg)
else: else:
msg = f"Unimplemented pixel format {repr(fourcc)}" msg = f"Unimplemented pixel format {repr(fourcc)}"
raise NotImplementedError(msg) raise NotImplementedError(msg)
else:
msg = f"Unknown pixel format flags {pfflags}"
raise NotImplementedError(msg)
if n:
self.tile = [ self.tile = [
("bcn", (0, 0) + self.size, data_start, (n, self.pixel_format)) ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
] ]
else:
self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)]
def load_seek(self, pos): def load_seek(self, pos):
pass pass
@ -274,48 +508,51 @@ def _save(im, fp, filename):
msg = f"cannot write mode {im.mode} as DDS" msg = f"cannot write mode {im.mode} as DDS"
raise OSError(msg) raise OSError(msg)
rawmode = im.mode alpha = im.mode[-1] == "A"
masks = [0xFF0000, 0xFF00, 0xFF] if im.mode[0] == "L":
if im.mode in ("L", "LA"): pixel_flags = DDPF.LUMINANCE
pixel_flags = DDPF_LUMINANCE rawmode = im.mode
if alpha:
rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF]
else:
rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000]
else: else:
pixel_flags = DDPF_RGB pixel_flags = DDPF.RGB
rawmode = rawmode[::-1] rawmode = im.mode[::-1]
if im.mode in ("LA", "RGBA"): rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
pixel_flags |= DDPF_ALPHAPIXELS
masks.append(0xFF000000)
bitcount = len(masks) * 8 if alpha:
while len(masks) < 4: r, g, b, a = im.split()
masks.append(0) im = Image.merge("RGBA", (a, r, g, b))
if alpha:
pixel_flags |= DDPF.ALPHAPIXELS
rgba_mask.append(0xFF000000 if alpha else 0)
flags = DDSD.CAPS | DDSD.HEIGHT | DDSD.WIDTH | DDSD.PITCH | DDSD.PIXELFORMAT
bitcount = len(im.getbands()) * 8
pitch = (im.width * bitcount + 7) // 8
fp.write( fp.write(
o32(DDS_MAGIC) o32(DDS_MAGIC)
+ o32(124) # header size + struct.pack(
+ o32( "<7I",
DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PITCH | DDSD_PIXELFORMAT 124, # header size
) # flags flags, # flags
+ o32(im.height) im.height,
+ o32(im.width) im.width,
+ o32((im.width * bitcount + 7) // 8) # pitch pitch,
+ o32(0) # depth 0, # depth
+ o32(0) # mipmaps 0, # mipmaps
+ o32(0) * 11 # reserved )
+ o32(32) # pfsize + struct.pack("11I", *((0,) * 11)) # reserved
+ o32(pixel_flags) # pfflags # pfsize, pfflags, fourcc, bitcount
+ o32(0) # fourcc + struct.pack("<4I", 32, pixel_flags, 0, bitcount)
+ o32(bitcount) # bitcount + struct.pack("<4I", *rgba_mask) # dwRGBABitMask
+ b"".join(o32(mask) for mask in masks) # rgbabitmask + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
+ o32(DDSCAPS_TEXTURE) # dwCaps )
+ o32(0) # dwCaps2 ImageFile._save(
+ o32(0) # dwCaps3 im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]
+ o32(0) # dwCaps4
+ o32(0) # dwReserved2
) )
if im.mode == "RGBA":
r, g, b, a = im.split()
im = Image.merge("RGBA", (a, r, g, b))
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
def _accept(prefix): def _accept(prefix):

View File

@ -97,16 +97,15 @@ class FpxImageFile(ImageFile.ImageFile):
s = prop[0x2000002 | id] s = prop[0x2000002 | id]
colors = []
bands = i32(s, 4) bands = i32(s, 4)
if bands > 4: if bands > 4:
msg = "Invalid number of bands" msg = "Invalid number of bands"
raise OSError(msg) 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 # load JPEG tables, if any
self.jpeg = {} self.jpeg = {}
@ -227,6 +226,7 @@ class FpxImageFile(ImageFile.ImageFile):
break # isn't really required break # isn't really required
self.stream = stream self.stream = stream
self._fp = self.fp
self.fp = None self.fp = None
def load(self): def load(self):

View File

@ -40,7 +40,7 @@ from enum import IntEnum
from pathlib import Path from pathlib import Path
try: try:
import defusedxml.ElementTree as ElementTree from defusedxml import ElementTree
except ImportError: except ImportError:
ElementTree = None ElementTree = None
@ -1160,7 +1160,7 @@ class Image:
if palette.mode != "P": if palette.mode != "P":
msg = "bad mode for palette image" msg = "bad mode for palette image"
raise ValueError(msg) raise ValueError(msg)
if self.mode != "RGB" and self.mode != "L": if self.mode not in {"RGB", "L"}:
msg = "only RGB or L mode images can be quantized to a palette" msg = "only RGB or L mode images can be quantized to a palette"
raise ValueError(msg) raise ValueError(msg)
im = self.im.convert("P", dither, palette.im) im = self.im.convert("P", dither, palette.im)
@ -1288,9 +1288,9 @@ class Image:
if self.im.bands == 1 or multiband: if self.im.bands == 1 or multiband:
return self._new(filter.filter(self.im)) return self._new(filter.filter(self.im))
ims = [] ims = [
for c in range(self.im.bands): self._new(filter.filter(self.im.getband(c))) for c in range(self.im.bands)
ims.append(self._new(filter.filter(self.im.getband(c)))) ]
return merge(self.mode, ims) return merge(self.mode, ims)
def getbands(self): def getbands(self):
@ -1339,10 +1339,7 @@ class Image:
self.load() self.load()
if self.mode in ("1", "L", "P"): if self.mode in ("1", "L", "P"):
h = self.im.histogram() h = self.im.histogram()
out = [] out = [(h[i], i) for i in range(256) if h[i]]
for i in range(256):
if h[i]:
out.append((h[i], i))
if len(out) > maxcolors: if len(out) > maxcolors:
return None return None
return out return out
@ -1383,10 +1380,7 @@ class Image:
self.load() self.load()
if self.im.bands > 1: if self.im.bands > 1:
extrema = [] return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands))
for i in range(self.im.bands):
extrema.append(self.im.getband(i).getextrema())
return tuple(extrema)
return self.im.getextrema() return self.im.getextrema()
def _getxmp(self, xmp_tags): def _getxmp(self, xmp_tags):

View File

@ -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 # info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint
description = profile.profile.profile_description description = profile.profile.profile_description
cpright = profile.profile.copyright cpright = profile.profile.copyright
arr = [] elements = [element for element in (description, cpright) if element]
for elt in (description, cpright): return "\r\n\r\n".join(elements) + "\r\n\r\n"
if elt:
arr.append(elt)
return "\r\n\r\n".join(arr) + "\r\n\r\n"
except (AttributeError, OSError, TypeError, ValueError) as v: except (AttributeError, OSError, TypeError, ValueError) as v:
raise PyCMSError(v) from v raise PyCMSError(v) from v

View File

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

View File

@ -26,11 +26,13 @@
# #
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
from __future__ import annotations
import io import io
import itertools import itertools
import struct import struct
import sys import sys
from typing import NamedTuple
from . import Image from . import Image
from ._util import is_path from ._util import is_path
@ -77,6 +79,13 @@ def _tilesort(t):
return t[2] return t[2]
class _Tile(NamedTuple):
encoder_name: str
extents: tuple[int, int, int, int]
offset: int
args: tuple | str | None
# #
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# ImageFile base class # ImageFile base class
@ -520,13 +529,13 @@ def _save(im, fp, tile, bufsize=0):
fp.flush() fp.flush()
def _encode_tile(im, fp, tile, bufsize, fh, exc=None): def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None):
for e, b, o, a in tile: for encoder_name, extents, offset, args in tile:
if o > 0: if offset > 0:
fp.seek(o) fp.seek(offset)
encoder = Image._getencoder(im.mode, e, a, im.encoderconfig) encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig)
try: try:
encoder.setimage(im.im, b) encoder.setimage(im.im, extents)
if encoder.pushes_fd: if encoder.pushes_fd:
encoder.setfd(fp) encoder.setfd(fp)
errcode = encoder.encode_to_pyfd()[1] errcode = encoder.encode_to_pyfd()[1]

View File

@ -25,12 +25,16 @@
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
from __future__ import annotations
import base64 import base64
import os import os
import sys import sys
import warnings import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from pathlib import Path
from typing import IO
from . import Image from . import Image
from ._util import is_directory, is_path from ._util import is_directory, is_path
@ -185,9 +189,20 @@ class ImageFont:
class FreeTypeFont: class FreeTypeFont:
"""FreeType font wrapper (requires _imagingft service)""" """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 # FIXME: use service provider instead
if size <= 0:
msg = "font size must be greater than 0"
raise ValueError(msg)
self.path = font self.path = font
self.size = size self.size = size
self.index = index self.index = index
@ -213,6 +228,8 @@ class FreeTypeFont:
) )
if is_path(font): if is_path(font):
if isinstance(font, Path):
font = str(font)
if sys.platform == "win32": if sys.platform == "win32":
font_bytes_path = font if isinstance(font, bytes) else font.encode() font_bytes_path = font if isinstance(font, bytes) else font.encode()
try: 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 This specifies the character set to use. It does not alter the
encoding of any text provided in subsequent operations. encoding of any text provided in subsequent operations.
:param layout_engine: Which layout engine to use, if available: :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. If it is available, Raqm layout will be used by default.
Otherwise, basic layout will be used. 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. :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): def freetype(font):
return FreeTypeFont(font, size, index, encoding, layout_engine) return FreeTypeFont(font, size, index, encoding, layout_engine)

View File

@ -557,9 +557,7 @@ def invert(image):
:param image: The image to invert. :param image: The image to invert.
:return: An image. :return: An image.
""" """
lut = [] lut = list(range(255, -1, -1))
for i in range(256):
lut.append(255 - i)
return image.point(lut) if image.mode == "1" else _lut(image, lut) 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). :param bits: The number of bits to keep for each channel (1-8).
:return: An image. :return: An image.
""" """
lut = []
mask = ~(2 ** (8 - bits) - 1) mask = ~(2 ** (8 - bits) - 1)
for i in range(256): lut = [i & mask for i in range(256)]
lut.append(i & mask)
return _lut(image, lut) return _lut(image, lut)

View File

@ -200,21 +200,15 @@ def raw(rawmode, data):
def make_linear_lut(black, white): def make_linear_lut(black, white):
lut = []
if black == 0: if black == 0:
for i in range(256): return [white * i // 255 for i in range(256)]
lut.append(white * i // 255)
else: msg = "unavailable when black is non-zero"
msg = "unavailable when black is non-zero" raise NotImplementedError(msg) # FIXME
raise NotImplementedError(msg) # FIXME
return lut
def make_gamma_lut(exp): def make_gamma_lut(exp):
lut = [] return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)]
for i in range(256):
lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5))
return lut
def negative(mode="RGB"): def negative(mode="RGB"):
@ -226,9 +220,7 @@ def negative(mode="RGB"):
def random(mode="RGB"): def random(mode="RGB"):
from random import randint from random import randint
palette = [] palette = [randint(0, 255) for _ in range(256 * len(mode))]
for i in range(256 * len(mode)):
palette.append(randint(0, 255))
return ImagePalette(mode, palette) return ImagePalette(mode, palette)

View File

@ -103,12 +103,10 @@ def align8to32(bytes, width, mode):
if not extra_padding: if not extra_padding:
return bytes return bytes
new_data = [] new_data = [
for i in range(len(bytes) // bytes_per_line): bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + b"\x00" * extra_padding
new_data.append( for i in range(len(bytes) // bytes_per_line)
bytes[i * bytes_per_line : (i + 1) * bytes_per_line] ]
+ b"\x00" * extra_padding
)
return b"".join(new_data) return b"".join(new_data)
@ -131,15 +129,11 @@ def _toqclass_helper(im):
format = qt_format.Format_Mono format = qt_format.Format_Mono
elif im.mode == "L": elif im.mode == "L":
format = qt_format.Format_Indexed8 format = qt_format.Format_Indexed8
colortable = [] colortable = [rgb(i, i, i) for i in range(256)]
for i in range(256):
colortable.append(rgb(i, i, i))
elif im.mode == "P": elif im.mode == "P":
format = qt_format.Format_Indexed8 format = qt_format.Format_Indexed8
colortable = []
palette = im.getpalette() palette = im.getpalette()
for i in range(0, len(palette), 3): colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)]
colortable.append(rgb(*palette[i : i + 3]))
elif im.mode == "RGB": elif im.mode == "RGB":
# Populate the 4th channel with 255 # Populate the 4th channel with 255
im = im.convert("RGBA") im = im.convert("RGBA")

View File

@ -21,9 +21,7 @@
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
import functools
import math import math
import operator
class Stat: class Stat:
@ -53,26 +51,22 @@ class Stat:
"""Get min/max values for each band in the image""" """Get min/max values for each band in the image"""
def minmax(histogram): def minmax(histogram):
n = 255 res_min, res_max = 255, 0
x = 0
for i in range(256): for i in range(256):
if histogram[i]: if histogram[i]:
n = min(n, i) res_min = i
x = max(x, i) break
return n, x # returns (255, 0) if there's no data in the histogram for i in range(255, -1, -1):
if histogram[i]:
res_max = i
break
return res_min, res_max
v = [] return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)]
for i in range(0, len(self.h), 256):
v.append(minmax(self.h[i:]))
return v
def _getcount(self): def _getcount(self):
"""Get total number of pixels in each layer""" """Get total number of pixels in each layer"""
return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)]
v = []
for i in range(0, len(self.h), 256):
v.append(functools.reduce(operator.add, self.h[i : i + 256]))
return v
def _getsum(self): def _getsum(self):
"""Get sum of all pixels in each layer""" """Get sum of all pixels in each layer"""
@ -98,11 +92,7 @@ class Stat:
def _getmean(self): def _getmean(self):
"""Get average pixel level for each layer""" """Get average pixel level for each layer"""
return [self.sum[i] / self.count[i] for i in self.bands]
v = []
for i in self.bands:
v.append(self.sum[i] / self.count[i])
return v
def _getmedian(self): def _getmedian(self):
"""Get median pixel level for each layer""" """Get median pixel level for each layer"""
@ -121,28 +111,18 @@ class Stat:
def _getrms(self): def _getrms(self):
"""Get RMS for each layer""" """Get RMS for each layer"""
return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
v = []
for i in self.bands:
v.append(math.sqrt(self.sum2[i] / self.count[i]))
return v
def _getvar(self): def _getvar(self):
"""Get variance for each layer""" """Get variance for each layer"""
return [
v = [] (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
for i in self.bands: for i in self.bands
n = self.count[i] ]
v.append((self.sum2[i] - (self.sum[i] ** 2.0) / n) / n)
return v
def _getstddev(self): def _getstddev(self):
"""Get standard deviation for each layer""" """Get standard deviation for each layer"""
return [math.sqrt(self.var[i]) for i in self.bands]
v = []
for i in self.bands:
v.append(math.sqrt(self.var[i]))
return v
Global = Stat # compatibility Global = Stat # compatibility

View File

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

View File

@ -233,9 +233,7 @@ def SOF(self, marker):
# fixup icc profile # fixup icc profile
self.icclist.sort() # sort by sequence number self.icclist.sort() # sort by sequence number
if self.icclist[0][13] == len(self.icclist): if self.icclist[0][13] == len(self.icclist):
profile = [] profile = [p[14:] for p in self.icclist]
for p in self.icclist:
profile.append(p[14:])
icc_profile = b"".join(profile) icc_profile = b"".join(profile)
else: else:
icc_profile = None # wrong number of fragments icc_profile = None # wrong number of fragments
@ -397,7 +395,7 @@ class JpegImageFile(ImageFile.ImageFile):
# self.__offset = self.fp.tell() # self.__offset = self.fp.tell()
break break
s = self.fp.read(1) s = self.fp.read(1)
elif i == 0 or i == 0xFFFF: elif i in {0, 0xFFFF}:
# padded marker or junk; move on # padded marker or junk; move on
s = b"\xff" s = b"\xff"
elif i == 0xFF00: # Skip extraneous data (escaped 0xFF) elif i == 0xFF00: # Skip extraneous data (escaped 0xFF)

View File

@ -51,10 +51,11 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
# find ACI subfiles with Image members (maybe not the # find ACI subfiles with Image members (maybe not the
# best way to identify MIC files, but what the... ;-) # best way to identify MIC files, but what the... ;-)
self.images = [] self.images = [
for path in self.ole.listdir(): path
if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image": for path in self.ole.listdir()
self.images.append(path) if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image"
]
# if we didn't find any images, this is probably not # if we didn't find any images, this is probably not
# an MIC file. # an MIC file.
@ -66,6 +67,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self._n_frames = len(self.images) self._n_frames = len(self.images)
self.is_animated = self._n_frames > 1 self.is_animated = self._n_frames > 1
self.__fp = self.fp
self.seek(0) self.seek(0)
def seek(self, frame): def seek(self, frame):
@ -87,10 +89,12 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
return self.frame return self.frame
def close(self): def close(self):
self.__fp.close()
self.ole.close() self.ole.close()
super().close() super().close()
def __exit__(self, *args): def __exit__(self, *args):
self.__fp.close()
self.ole.close() self.ole.close()
super().__exit__() super().__exit__()

View File

@ -129,9 +129,8 @@ class PcfFontFile(FontFile.FontFile):
nprops = i32(fp.read(4)) nprops = i32(fp.read(4))
# read property description # read property description
p = [] p = [(i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))) for _ in range(nprops)]
for i in range(nprops):
p.append((i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))))
if nprops & 3: if nprops & 3:
fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad
@ -186,8 +185,6 @@ class PcfFontFile(FontFile.FontFile):
# #
# bitmap data # bitmap data
bitmaps = []
fp, format, i16, i32 = self._getformat(PCF_BITMAPS) fp, format, i16, i32 = self._getformat(PCF_BITMAPS)
nbitmaps = i32(fp.read(4)) nbitmaps = i32(fp.read(4))
@ -196,13 +193,9 @@ class PcfFontFile(FontFile.FontFile):
msg = "Wrong number of bitmaps" msg = "Wrong number of bitmaps"
raise OSError(msg) raise OSError(msg)
offsets = [] offsets = [i32(fp.read(4)) for _ in range(nbitmaps)]
for i in range(nbitmaps):
offsets.append(i32(fp.read(4)))
bitmap_sizes = [] bitmap_sizes = [i32(fp.read(4)) for _ in range(4)]
for i in range(4):
bitmap_sizes.append(i32(fp.read(4)))
# byteorder = format & 4 # non-zero => MSB # byteorder = format & 4 # non-zero => MSB
bitorder = format & 8 # non-zero => MSB bitorder = format & 8 # non-zero => MSB
@ -218,6 +211,7 @@ class PcfFontFile(FontFile.FontFile):
if bitorder: if bitorder:
mode = "1" mode = "1"
bitmaps = []
for i in range(nbitmaps): for i in range(nbitmaps):
xsize, ysize = metrics[i][:2] xsize, ysize = metrics[i][:2]
b, e = offsets[i : i + 2] b, e = offsets[i : i + 2]

View File

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

View File

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

View File

@ -238,9 +238,7 @@ def makeSpiderHeader(im):
if nvalues < 23: if nvalues < 23:
return [] return []
hdr = [] hdr = [0.0] * nvalues
for i in range(nvalues):
hdr.append(0.0)
# NB these are Fortran indices # NB these are Fortran indices
hdr[1] = 1.0 # nslice (=1 for an image) hdr[1] = 1.0 # nslice (=1 for an image)

View File

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

View File

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