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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 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():

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)
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:]
)

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_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

View File

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

View File

@ -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):

View File

@ -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"
)

View File

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

View File

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

View File

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

View File

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

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::
:maxdepth: 2
10.2.0
10.1.0
10.0.1
10.0.0

View File

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

View File

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

View File

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

View File

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

View File

@ -3,110 +3,322 @@ A Pillow loader for .dds files (S3TC-compressed aka DXTC)
Jerome Leclanche <jerome@leclan.ch>
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("<I", header.read(4))
if pfflags & DDPF_LUMINANCE:
# 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:
pfsize, pfflags, fourcc, bitcount = struct.unpack("<4I", header.read(16))
n = 0
rawmode = None
if pfflags & DDPF.RGB:
# Texture contains uncompressed RGB data
if pfflags & DDPF_ALPHAPIXELS:
if pfflags & DDPF.ALPHAPIXELS:
self._mode = "RGBA"
mask_count = 4
else:
self._mode = "RGB"
mask_count = 3
masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4))
self.tile = [("dds_rgb", (0, 0) + self.size, 0, (bitcount, masks))]
elif pfflags & DDPF_PALETTEINDEXED8:
self.tile = [("dds_rgb", extents, 0, (bitcount, masks))]
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.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
self.tile = [("raw", (0, 0) + self.size, 0, "L")]
else:
data_start = header_size + 4
n = 0
if fourcc == b"DXT1":
elif pfflags & DDPF.FOURCC:
offset = header_size + 4
if fourcc == D3DFMT.DXT1:
self._mode = "RGBA"
self.pixel_format = "DXT1"
n = 1
elif fourcc == b"DXT3":
elif fourcc == D3DFMT.DXT3:
self._mode = "RGBA"
self.pixel_format = "DXT3"
n = 2
elif fourcc == b"DXT5":
elif fourcc == D3DFMT.DXT5:
self._mode = "RGBA"
self.pixel_format = "DXT5"
n = 3
elif fourcc == b"ATI1":
elif fourcc in (D3DFMT.BC4U, D3DFMT.ATI1):
self._mode = "L"
self.pixel_format = "BC4"
n = 4
self._mode = "L"
elif fourcc in (b"ATI2", b"BC5U"):
self.pixel_format = "BC5"
n = 5
elif fourcc == D3DFMT.BC5S:
self._mode = "RGB"
elif fourcc == b"BC5S":
self.pixel_format = "BC5S"
n = 5
elif fourcc in (D3DFMT.BC5U, D3DFMT.ATI2):
self._mode = "RGB"
elif fourcc == b"DX10":
data_start += 20
self.pixel_format = "BC5"
n = 5
elif fourcc == D3DFMT.DX10:
offset += 20
# ignoring flags which pertain to volume textures and cubemaps
(dxgi_format,) = struct.unpack("<I", self.fp.read(4))
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"
n = 5
elif dxgi_format == DXGI_FORMAT.BC5_SNORM:
self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC5_SNORM:
self.pixel_format = "BC5S"
n = 5
elif dxgi_format == DXGI_FORMAT.BC6H_UF16:
self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_UF16:
self.pixel_format = "BC6H"
n = 6
elif dxgi_format == DXGI_FORMAT.BC6H_SF16:
self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_SF16:
self.pixel_format = "BC6HS"
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 (
DXGI_FORMAT_R8G8B8A8_TYPELESS,
DXGI_FORMAT_R8G8B8A8_UNORM,
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
DXGI_FORMAT.BC7_TYPELESS,
DXGI_FORMAT.BC7_UNORM,
DXGI_FORMAT.BC7_UNORM_SRGB,
):
self.tile = [("raw", (0, 0) + self.size, 0, ("RGBA", 0, 1))]
if dxgi_format == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
self._mode = "RGBA"
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
return
else:
msg = f"Unimplemented DXGI format {dxgi_format}"
raise NotImplementedError(msg)
else:
msg = f"Unimplemented pixel format {repr(fourcc)}"
raise NotImplementedError(msg)
else:
msg = f"Unknown pixel format flags {pfflags}"
raise NotImplementedError(msg)
if n:
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):
pass
@ -274,48 +508,51 @@ def _save(im, fp, filename):
msg = f"cannot write mode {im.mode} as DDS"
raise OSError(msg)
rawmode = im.mode
masks = [0xFF0000, 0xFF00, 0xFF]
if im.mode in ("L", "LA"):
pixel_flags = DDPF_LUMINANCE
alpha = im.mode[-1] == "A"
if im.mode[0] == "L":
pixel_flags = DDPF.LUMINANCE
rawmode = im.mode
if alpha:
rgba_mask = [0x000000FF, 0x000000FF, 0x000000FF]
else:
rgba_mask = [0xFF000000, 0xFF000000, 0xFF000000]
else:
pixel_flags = DDPF_RGB
rawmode = rawmode[::-1]
if im.mode in ("LA", "RGBA"):
pixel_flags |= DDPF_ALPHAPIXELS
masks.append(0xFF000000)
pixel_flags = DDPF.RGB
rawmode = im.mode[::-1]
rgba_mask = [0x00FF0000, 0x0000FF00, 0x000000FF]
bitcount = len(masks) * 8
while len(masks) < 4:
masks.append(0)
if alpha:
r, g, b, a = im.split()
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(
o32(DDS_MAGIC)
+ o32(124) # header size
+ o32(
DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PITCH | DDSD_PIXELFORMAT
) # flags
+ o32(im.height)
+ o32(im.width)
+ o32((im.width * bitcount + 7) // 8) # pitch
+ o32(0) # depth
+ o32(0) # mipmaps
+ o32(0) * 11 # reserved
+ o32(32) # pfsize
+ o32(pixel_flags) # pfflags
+ o32(0) # fourcc
+ o32(bitcount) # bitcount
+ b"".join(o32(mask) for mask in masks) # rgbabitmask
+ o32(DDSCAPS_TEXTURE) # dwCaps
+ o32(0) # dwCaps2
+ o32(0) # dwCaps3
+ o32(0) # dwCaps4
+ o32(0) # dwReserved2
+ struct.pack(
"<7I",
124, # header size
flags, # flags
im.height,
im.width,
pitch,
0, # depth
0, # mipmaps
)
+ struct.pack("11I", *((0,) * 11)) # reserved
# pfsize, pfflags, fourcc, bitcount
+ struct.pack("<4I", 32, pixel_flags, 0, bitcount)
+ struct.pack("<4I", *rgba_mask) # dwRGBABitMask
+ struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
)
ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]
)
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):

View File

@ -97,16 +97,15 @@ class FpxImageFile(ImageFile.ImageFile):
s = prop[0x2000002 | id]
colors = []
bands = i32(s, 4)
if bands > 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):

View File

@ -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):

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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")

View File

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

View File

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

View File

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

View File

@ -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__()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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