mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-01-27 09:44:31 +03:00
Merge branch 'main' into _fp
This commit is contained in:
commit
791c5aa21c
|
@ -1,99 +0,0 @@
|
|||
skip_commits:
|
||||
files:
|
||||
- ".github/**/*"
|
||||
- ".gitmodules"
|
||||
- "docs/**/*"
|
||||
- "wheels/**/*"
|
||||
|
||||
version: '{build}'
|
||||
clone_folder: c:\pillow
|
||||
init:
|
||||
- ECHO %PYTHON%
|
||||
#- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
||||
# Uncomment previous line to get RDP access during the build.
|
||||
|
||||
environment:
|
||||
COVERAGE_CORE: sysmon
|
||||
EXECUTABLE: python.exe
|
||||
TEST_OPTIONS:
|
||||
DEPLOY: YES
|
||||
matrix:
|
||||
- PYTHON: C:/Python313
|
||||
ARCHITECTURE: x86
|
||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
||||
- PYTHON: C:/Python39-x64
|
||||
ARCHITECTURE: AMD64
|
||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
|
||||
|
||||
|
||||
install:
|
||||
- '%PYTHON%\%EXECUTABLE% --version'
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
|
||||
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
||||
- 7z x pillow-test-images.zip -oc:\
|
||||
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
|
||||
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
|
||||
- 7z x nasm-win64.zip -oc:\
|
||||
- choco install ghostscript --version=10.4.0
|
||||
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.04.0\bin;%PATH%
|
||||
- cd c:\pillow\winbuild\
|
||||
- ps: |
|
||||
c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
||||
c:\pillow\winbuild\build\build_dep_all.cmd
|
||||
$host.SetShouldExit(0)
|
||||
- path C:\pillow\winbuild\build\bin;%PATH%
|
||||
|
||||
build_script:
|
||||
- cd c:\pillow
|
||||
- winbuild\build\build_env.cmd
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .'
|
||||
- '%PYTHON%\%EXECUTABLE% selftest.py --installed'
|
||||
|
||||
test_script:
|
||||
- cd c:\pillow
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
|
||||
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
|
||||
- path %PYTHON%;%PATH%
|
||||
- .ci\test.cmd
|
||||
|
||||
after_test:
|
||||
- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe
|
||||
- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
cache:
|
||||
- '%LOCALAPPDATA%\pip\Cache'
|
||||
|
||||
artifacts:
|
||||
- path: pillow\*.egg
|
||||
name: egg
|
||||
- path: pillow\*.whl
|
||||
name: wheel
|
||||
|
||||
before_deploy:
|
||||
- cd c:\pillow
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .'
|
||||
- ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
|
||||
|
||||
deploy:
|
||||
provider: S3
|
||||
region: us-west-2
|
||||
access_key_id: AKIAIRAXC62ZNTVQJMOQ
|
||||
secret_access_key:
|
||||
secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi
|
||||
bucket: pillow-nightly
|
||||
folder: win/$(APPVEYOR_BUILD_NUMBER)/
|
||||
artifact: /.*egg|wheel/
|
||||
on:
|
||||
APPVEYOR_REPO_NAME: python-pillow/Pillow
|
||||
branch: main
|
||||
deploy: YES
|
||||
|
||||
|
||||
# Uncomment the following lines to get RDP access after the build/test and block for
|
||||
# up to the timeout limit (~1hr)
|
||||
#
|
||||
#on_finish:
|
||||
#- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
|
@ -2,8 +2,4 @@
|
|||
|
||||
# gather the coverage data
|
||||
python3 -m pip install coverage
|
||||
if [[ $MATRIX_DOCKER ]]; then
|
||||
python3 -m coverage xml --ignore-errors
|
||||
else
|
||||
python3 -m coverage xml
|
||||
fi
|
||||
python3 -m coverage xml
|
||||
|
|
|
@ -3,8 +3,5 @@
|
|||
set -e
|
||||
|
||||
python3 -m coverage erase
|
||||
if [ $(uname) == "Darwin" ]; then
|
||||
export CPPFLAGS="-I/usr/local/miniconda/include";
|
||||
fi
|
||||
make clean
|
||||
make install-coverage
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mypy==1.14.0
|
||||
mypy==1.14.1
|
||||
IceSpringPySideStubs-PyQt6
|
||||
IceSpringPySideStubs-PySide6
|
||||
ipython
|
||||
|
|
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
|
@ -9,7 +9,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
|
|||
- Fork the Pillow repository.
|
||||
- Create a branch from `main`.
|
||||
- Develop bug fixes, features, tests, etc.
|
||||
- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
|
||||
- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
|
||||
- Create a pull request to pull the changes from your branch to the Pillow `main`.
|
||||
|
||||
### Guidelines
|
||||
|
@ -17,7 +17,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
|
|||
- Separate code commits from reformatting commits.
|
||||
- Provide tests for any newly added code.
|
||||
- Follow PEP 8.
|
||||
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor.
|
||||
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running extra tests.
|
||||
- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests.
|
||||
|
||||
## Reporting Issues
|
||||
|
|
1
.github/mergify.yml
vendored
1
.github/mergify.yml
vendored
|
@ -9,7 +9,6 @@ pull_request_rules:
|
|||
- status-success=Windows Test Successful
|
||||
- status-success=MinGW
|
||||
- status-success=Cygwin Test Successful
|
||||
- status-success=continuous-integration/appveyor/pr
|
||||
actions:
|
||||
merge:
|
||||
method: merge
|
||||
|
|
2
.github/workflows/test-cygwin.yml
vendored
2
.github/workflows/test-cygwin.yml
vendored
|
@ -52,7 +52,7 @@ jobs:
|
|||
persist-credentials: false
|
||||
|
||||
- name: Install Cygwin
|
||||
uses: cygwin/cygwin-install-action@v4
|
||||
uses: cygwin/cygwin-install-action@v5
|
||||
with:
|
||||
packages: >
|
||||
gcc-g++
|
||||
|
|
16
.github/workflows/test-docker.yml
vendored
16
.github/workflows/test-docker.yml
vendored
|
@ -29,13 +29,13 @@ concurrency:
|
|||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest"]
|
||||
docker: [
|
||||
# Run slower jobs first to give them a headstart and reduce waiting time
|
||||
ubuntu-22.04-jammy-arm64v8,
|
||||
ubuntu-24.04-noble-ppc64le,
|
||||
ubuntu-24.04-noble-s390x,
|
||||
# Then run the remainder
|
||||
|
@ -44,6 +44,7 @@ jobs:
|
|||
amazon-2023-amd64,
|
||||
arch,
|
||||
centos-stream-9-amd64,
|
||||
centos-stream-10-amd64,
|
||||
debian-12-bookworm-x86,
|
||||
debian-12-bookworm-amd64,
|
||||
fedora-40-amd64,
|
||||
|
@ -54,12 +55,13 @@ jobs:
|
|||
]
|
||||
dockerTag: [main]
|
||||
include:
|
||||
- docker: "ubuntu-22.04-jammy-arm64v8"
|
||||
qemu-arch: "aarch64"
|
||||
- docker: "ubuntu-24.04-noble-ppc64le"
|
||||
qemu-arch: "ppc64le"
|
||||
- docker: "ubuntu-24.04-noble-s390x"
|
||||
qemu-arch: "s390x"
|
||||
- docker: "ubuntu-24.04-noble-arm64v8"
|
||||
os: "ubuntu-24.04-arm"
|
||||
dockerTag: main
|
||||
|
||||
name: ${{ matrix.docker }}
|
||||
|
||||
|
@ -89,15 +91,15 @@ jobs:
|
|||
|
||||
- name: After success
|
||||
run: |
|
||||
PATH="$PATH:~/.local/bin"
|
||||
docker start pillow_container
|
||||
sudo docker cp pillow_container:/Pillow /Pillow
|
||||
sudo chown -R runner /Pillow
|
||||
pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'`
|
||||
docker stop pillow_container
|
||||
sudo mkdir -p $pil_path
|
||||
sudo cp src/PIL/*.py $pil_path
|
||||
cd /Pillow
|
||||
.ci/after_success.sh
|
||||
env:
|
||||
MATRIX_DOCKER: ${{ matrix.docker }}
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
|
|
6
.github/workflows/test-mingw.yml
vendored
6
.github/workflows/test-mingw.yml
vendored
|
@ -66,9 +66,9 @@ jobs:
|
|||
mingw-w64-x86_64-libtiff \
|
||||
mingw-w64-x86_64-libwebp \
|
||||
mingw-w64-x86_64-openjpeg2 \
|
||||
mingw-w64-x86_64-python3-numpy \
|
||||
mingw-w64-x86_64-python3-olefile \
|
||||
mingw-w64-x86_64-python3-pip \
|
||||
mingw-w64-x86_64-python-numpy \
|
||||
mingw-w64-x86_64-python-olefile \
|
||||
mingw-w64-x86_64-python-pip \
|
||||
mingw-w64-x86_64-python-pytest \
|
||||
mingw-w64-x86_64-python-pytest-cov \
|
||||
mingw-w64-x86_64-python-pytest-timeout \
|
||||
|
|
14
.github/workflows/test-windows.yml
vendored
14
.github/workflows/test-windows.yml
vendored
|
@ -31,15 +31,20 @@ env:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
python-version: ["pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
architecture: ["x64"]
|
||||
os: ["windows-latest"]
|
||||
include:
|
||||
# Test the oldest Python on 32-bit
|
||||
- { python-version: "3.9", architecture: "x86", os: "windows-2019" }
|
||||
|
||||
timeout-minutes: 30
|
||||
|
||||
name: Python ${{ matrix.python-version }}
|
||||
name: Python ${{ matrix.python-version }} (${{ matrix.architecture }})
|
||||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
|
@ -67,6 +72,7 @@ jobs:
|
|||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
architecture: ${{ matrix.architecture }}
|
||||
cache: pip
|
||||
cache-dependency-path: ".github/workflows/test-windows.yml"
|
||||
|
||||
|
@ -78,7 +84,7 @@ jobs:
|
|||
python3 -m pip install --upgrade pip
|
||||
|
||||
- name: Install CPython dependencies
|
||||
if: "!contains(matrix.python-version, 'pypy')"
|
||||
if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'"
|
||||
run: |
|
||||
python3 -m pip install PyQt6
|
||||
|
||||
|
|
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
|
@ -42,6 +42,7 @@ jobs:
|
|||
]
|
||||
python-version: [
|
||||
"pypy3.10",
|
||||
"3.14",
|
||||
"3.13t",
|
||||
"3.13",
|
||||
"3.12",
|
||||
|
|
17
.github/workflows/wheels-dependencies.sh
vendored
17
.github/workflows/wheels-dependencies.sh
vendored
|
@ -37,20 +37,15 @@ fi
|
|||
ARCHIVE_SDIR=pillow-depends-main
|
||||
|
||||
# Package versions for fresh source builds
|
||||
FREETYPE_VERSION=2.13.2
|
||||
FREETYPE_VERSION=2.13.3
|
||||
HARFBUZZ_VERSION=10.1.0
|
||||
LIBPNG_VERSION=1.6.44
|
||||
LIBPNG_VERSION=1.6.45
|
||||
JPEGTURBO_VERSION=3.1.0
|
||||
OPENJPEG_VERSION=2.5.3
|
||||
XZ_VERSION=5.6.3
|
||||
TIFF_VERSION=4.6.0
|
||||
LCMS2_VERSION=2.16
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
GIFLIB_VERSION=5.2.2
|
||||
else
|
||||
GIFLIB_VERSION=5.2.1
|
||||
fi
|
||||
ZLIB_NG_VERSION=2.2.2
|
||||
ZLIB_NG_VERSION=2.2.3
|
||||
LIBWEBP_VERSION=1.5.0
|
||||
BZIP2_VERSION=1.0.8
|
||||
LIBXCB_VERSION=1.17.0
|
||||
|
@ -103,7 +98,7 @@ function build_harfbuzz {
|
|||
|
||||
function build {
|
||||
build_xz
|
||||
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
|
||||
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
|
||||
yum remove -y zlib-devel
|
||||
fi
|
||||
build_zlib_ng
|
||||
|
@ -140,7 +135,9 @@ function build {
|
|||
if [[ -n "$IS_MACOS" ]]; then
|
||||
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
|
||||
fi
|
||||
build_libwebp
|
||||
build_simple libwebp $LIBWEBP_VERSION \
|
||||
https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
|
||||
--enable-libwebpmux --enable-libwebpdemux
|
||||
CFLAGS=$ORIGINAL_CFLAGS
|
||||
|
||||
build_brotli
|
||||
|
|
3
.github/workflows/wheels-test.ps1
vendored
3
.github/workflows/wheels-test.ps1
vendored
|
@ -11,6 +11,9 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
|
|||
$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
|
||||
if ("$venv" -like "*\cibw-run-*-win_amd64\*") {
|
||||
& python -m pip install numpy
|
||||
}
|
||||
cd $pillow
|
||||
& python -VV
|
||||
if (!$?) { exit $LASTEXITCODE }
|
||||
|
|
77
.github/workflows/wheels.yml
vendored
77
.github/workflows/wheels.yml
vendored
|
@ -13,6 +13,7 @@ on:
|
|||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "pyproject.toml"
|
||||
- "setup.py"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
|
@ -23,6 +24,7 @@ on:
|
|||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "pyproject.toml"
|
||||
- "setup.py"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
|
@ -40,62 +42,7 @@ env:
|
|||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
build-1-QEMU-emulated-wheels:
|
||||
if: github.event_name != 'schedule'
|
||||
name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version:
|
||||
- pp310
|
||||
- cp3{9,10,11}
|
||||
- cp3{12,13}
|
||||
spec:
|
||||
- manylinux2014
|
||||
- manylinux_2_28
|
||||
- musllinux
|
||||
exclude:
|
||||
- { python-version: pp310, spec: musllinux }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
# https://github.com/docker/setup-qemu-action
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Install cibuildwheel
|
||||
run: |
|
||||
python3 -m pip install -r .ci/requirements-cibw.txt
|
||||
|
||||
- name: Build wheels
|
||||
run: |
|
||||
python3 -m cibuildwheel --output-dir wheelhouse
|
||||
env:
|
||||
# Build only the currently selected Linux architecture (so we can
|
||||
# parallelise for speed).
|
||||
CIBW_ARCHS: "aarch64"
|
||||
# Likewise, select only one Python version per job to speed this up.
|
||||
CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
|
||||
CIBW_ENABLE: cpython-prerelease
|
||||
# Extra options for manylinux.
|
||||
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
|
||||
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }}
|
||||
path: ./wheelhouse/*.whl
|
||||
|
||||
build-2-native-wheels:
|
||||
build-native-wheels:
|
||||
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
@ -130,6 +77,14 @@ jobs:
|
|||
cibw_arch: x86_64
|
||||
build: "*manylinux*"
|
||||
manylinux: "manylinux_2_28"
|
||||
- name: "manylinux2014 and musllinux aarch64"
|
||||
os: ubuntu-24.04-arm
|
||||
cibw_arch: aarch64
|
||||
- name: "manylinux_2_28 aarch64"
|
||||
os: ubuntu-24.04-arm
|
||||
cibw_arch: aarch64
|
||||
build: "*manylinux*"
|
||||
manylinux: "manylinux_2_28"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
@ -150,7 +105,9 @@ jobs:
|
|||
env:
|
||||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||
CIBW_BUILD: ${{ matrix.build }}
|
||||
CIBW_ENABLE: cpython-prerelease cpython-freethreading
|
||||
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
|
||||
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_SKIP: pp39-*
|
||||
|
@ -227,7 +184,7 @@ jobs:
|
|||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
||||
CIBW_CACHE_PATH: "C:\\cibw"
|
||||
CIBW_ENABLE: cpython-prerelease cpython-freethreading
|
||||
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
|
||||
CIBW_SKIP: pp39-*
|
||||
CIBW_TEST_SKIP: "*-win_arm64"
|
||||
CIBW_TEST_COMMAND: 'docker run --rm
|
||||
|
@ -273,7 +230,7 @@ jobs:
|
|||
|
||||
scientific-python-nightly-wheels-publish:
|
||||
if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||
needs: [build-2-native-wheels, windows]
|
||||
needs: [build-native-wheels, windows]
|
||||
runs-on: ubuntu-latest
|
||||
name: Upload wheels to scientific-python-nightly-wheels
|
||||
steps:
|
||||
|
@ -290,7 +247,7 @@ jobs:
|
|||
|
||||
pypi-publish:
|
||||
if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]
|
||||
needs: [build-native-wheels, windows, sdist]
|
||||
runs-on: ubuntu-latest
|
||||
name: Upload release to PyPI
|
||||
environment:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.4
|
||||
rev: v0.8.6
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--exit-non-zero-on-fix]
|
||||
|
@ -24,7 +24,7 @@ repos:
|
|||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v19.1.5
|
||||
rev: v19.1.6
|
||||
hooks:
|
||||
- id: clang-format
|
||||
types: [c]
|
||||
|
@ -57,7 +57,7 @@ repos:
|
|||
- id: check-renovate
|
||||
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v0.10.0
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
version: 2
|
||||
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
formats: [pdf]
|
||||
|
||||
build:
|
||||
|
|
|
@ -20,7 +20,6 @@ graft docs
|
|||
graft _custom_build
|
||||
|
||||
# build/src control detritus
|
||||
exclude .appveyor.yml
|
||||
exclude .clang-format
|
||||
exclude .coveragerc
|
||||
exclude .editorconfig
|
||||
|
|
|
@ -42,9 +42,6 @@ As of 2019, Pillow development is
|
|||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
|
||||
alt="GitHub Actions build status (Test Docker)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>
|
||||
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
|
||||
alt="AppVeyor CI build status (Windows)"
|
||||
src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a>
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
|
||||
alt="GitHub Actions build status (Wheels)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
|
||||
|
|
|
@ -9,7 +9,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
|||
|
||||
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
|
||||
* [ ] Develop and prepare release in `main` branch.
|
||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
|
||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch.
|
||||
* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them.
|
||||
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
||||
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
|
||||
|
@ -38,7 +38,7 @@ Released as needed for security, installation or critical bug fixes.
|
|||
git checkout -t remotes/origin/5.2.x
|
||||
```
|
||||
* [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`.
|
||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`.
|
||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in release branch e.g. `5.2.x`.
|
||||
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
||||
* [ ] Run pre-release check via `make release-test`.
|
||||
* [ ] Create tag for release e.g.:
|
||||
|
|
|
@ -140,18 +140,11 @@ def assert_image_similar_tofile(
|
|||
filename: str,
|
||||
epsilon: float,
|
||||
msg: str | None = None,
|
||||
mode: str | None = None,
|
||||
) -> None:
|
||||
with Image.open(filename) as img:
|
||||
if mode:
|
||||
img = img.convert(mode)
|
||||
assert_image_similar(a, img, epsilon, msg)
|
||||
|
||||
|
||||
def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None:
|
||||
assert items.count(items[0]) == len(items), msg
|
||||
|
||||
|
||||
def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
|
||||
assert items.count(items[0]) != len(items), msg
|
||||
|
||||
|
@ -327,16 +320,7 @@ def magick_command() -> list[str] | None:
|
|||
return None
|
||||
|
||||
|
||||
def on_appveyor() -> bool:
|
||||
return "APPVEYOR" in os.environ
|
||||
|
||||
|
||||
def on_github_actions() -> bool:
|
||||
return "GITHUB_ACTIONS" in os.environ
|
||||
|
||||
|
||||
def on_ci() -> bool:
|
||||
# GitHub Actions and AppVeyor have "CI"
|
||||
return "CI" in os.environ
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import fuzzers
|
|||
import packaging
|
||||
import pytest
|
||||
|
||||
from PIL import Image, UnidentifiedImageError, features
|
||||
from PIL import Image, features
|
||||
from Tests.helper import skip_unless_feature
|
||||
|
||||
if sys.platform.startswith("win32"):
|
||||
|
@ -32,21 +32,17 @@ def test_fuzz_images(path: str) -> None:
|
|||
fuzzers.fuzz_image(f.read())
|
||||
assert True
|
||||
except (
|
||||
# Known exceptions from Pillow
|
||||
OSError,
|
||||
SyntaxError,
|
||||
MemoryError,
|
||||
ValueError,
|
||||
NotImplementedError,
|
||||
OverflowError,
|
||||
):
|
||||
# Known exceptions that are through from Pillow
|
||||
assert True
|
||||
except (
|
||||
# Known Image.* exceptions
|
||||
Image.DecompressionBombError,
|
||||
Image.DecompressionBombWarning,
|
||||
UnidentifiedImageError,
|
||||
):
|
||||
# Known Image.* exceptions
|
||||
assert True
|
||||
finally:
|
||||
fuzzers.disable_decompressionbomb_error()
|
||||
|
|
|
@ -307,13 +307,8 @@ def test_apng_syntax_errors() -> None:
|
|||
im.load()
|
||||
|
||||
# we can handle this case gracefully
|
||||
exception = None
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
|
||||
try:
|
||||
im.seek(im.n_frames - 1)
|
||||
except Exception as e:
|
||||
exception = e
|
||||
assert exception is None
|
||||
im.seek(im.n_frames - 1)
|
||||
|
||||
with pytest.raises(OSError):
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im:
|
||||
|
@ -405,13 +400,8 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
|
|||
append_images=frames,
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
exception = None
|
||||
try:
|
||||
im.seek(im.n_frames - 1)
|
||||
im.load()
|
||||
except Exception as e:
|
||||
exception = e
|
||||
assert exception is None
|
||||
im.seek(im.n_frames - 1)
|
||||
im.load()
|
||||
|
||||
|
||||
def test_apng_save_duration_loop(tmp_path: Path) -> None:
|
||||
|
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import BlpImagePlugin, Image
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -19,6 +19,7 @@ def test_load_blp1() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
|
||||
|
||||
with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im:
|
||||
assert im.mode == "RGBA"
|
||||
im.load()
|
||||
|
||||
|
||||
|
@ -37,6 +38,13 @@ def test_load_blp2_dxt1a() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
with pytest.raises(BlpImagePlugin.BLPFormatError):
|
||||
BlpImagePlugin.BlpImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_save(tmp_path: Path) -> None:
|
||||
f = str(tmp_path / "temp.blp")
|
||||
|
||||
|
|
|
@ -253,8 +253,7 @@ def test_truncated_mask() -> None:
|
|||
|
||||
try:
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
with Image.open("Tests/images/hopper_mask.png") as expected:
|
||||
assert im.mode == "1"
|
||||
assert im.mode == "1"
|
||||
|
||||
# 32 bpp
|
||||
output = io.BytesIO()
|
||||
|
|
|
@ -58,10 +58,7 @@ def test_getiptcinfo_fotostation() -> None:
|
|||
|
||||
# Assert
|
||||
assert iptc is not None
|
||||
for tag in iptc.keys():
|
||||
if tag[0] == 240:
|
||||
return
|
||||
pytest.fail("FotoStation tag not found")
|
||||
assert 240 in (tag[0] for tag in iptc.keys()), "FotoStation tag not found"
|
||||
|
||||
|
||||
def test_getiptcinfo_zero_padding() -> None:
|
||||
|
|
|
@ -181,7 +181,7 @@ class TestFileJpeg:
|
|||
assert test(100, 200) == (100, 200)
|
||||
assert test(0) is None # square pixels
|
||||
|
||||
def test_dpi_jfif_cm(self):
|
||||
def test_dpi_jfif_cm(self) -> None:
|
||||
with Image.open("Tests/images/jfif_unit_cm.jpg") as im:
|
||||
assert im.info["dpi"] == (2.54, 5.08)
|
||||
|
||||
|
@ -281,7 +281,10 @@ class TestFileJpeg:
|
|||
assert not im2.info.get("progressive")
|
||||
assert im3.info.get("progressive")
|
||||
|
||||
assert_image_equal(im1, im3)
|
||||
if features.check_feature("mozjpeg"):
|
||||
assert_image_similar(im1, im3, 9.39)
|
||||
else:
|
||||
assert_image_equal(im1, im3)
|
||||
assert im1_bytes >= im3_bytes
|
||||
|
||||
def test_progressive_large_buffer(self, tmp_path: Path) -> None:
|
||||
|
@ -353,7 +356,6 @@ class TestFileJpeg:
|
|||
assert exif.get_ifd(0x8825) == {}
|
||||
|
||||
transposed = ImageOps.exif_transpose(im)
|
||||
assert transposed is not None
|
||||
exif = transposed.getexif()
|
||||
assert exif.get_ifd(0x8825) == {}
|
||||
|
||||
|
@ -424,8 +426,12 @@ class TestFileJpeg:
|
|||
|
||||
im2 = self.roundtrip(hopper(), progressive=1)
|
||||
im3 = self.roundtrip(hopper(), progression=1) # compatibility
|
||||
assert_image_equal(im1, im2)
|
||||
assert_image_equal(im1, im3)
|
||||
if features.check_feature("mozjpeg"):
|
||||
assert_image_similar(im1, im2, 9.39)
|
||||
assert_image_similar(im1, im3, 9.39)
|
||||
else:
|
||||
assert_image_equal(im1, im2)
|
||||
assert_image_equal(im1, im3)
|
||||
assert im2.info.get("progressive")
|
||||
assert im2.info.get("progression")
|
||||
assert im3.info.get("progressive")
|
||||
|
@ -1031,7 +1037,7 @@ class TestFileJpeg:
|
|||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.tile = [
|
||||
("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)),
|
||||
ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)),
|
||||
]
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
im.load()
|
||||
|
|
|
@ -492,8 +492,7 @@ def test_plt_marker(card: ImageFile.ImageFile) -> None:
|
|||
out.seek(0)
|
||||
while True:
|
||||
marker = out.read(2)
|
||||
if not marker:
|
||||
pytest.fail("End of stream without PLT")
|
||||
assert marker, "End of stream without PLT"
|
||||
|
||||
jp2_boxid = _binary.i16be(marker)
|
||||
if jp2_boxid == 0xFF4F:
|
||||
|
|
|
@ -36,11 +36,7 @@ class LibTiffTestCase:
|
|||
im.load()
|
||||
im.getdata()
|
||||
|
||||
try:
|
||||
assert im._compression == "group4"
|
||||
except AttributeError:
|
||||
print("No _compression")
|
||||
print(dir(im))
|
||||
assert im._compression == "group4"
|
||||
|
||||
# can we write it back out, in a different form.
|
||||
out = str(tmp_path / "temp.png")
|
||||
|
@ -1146,7 +1142,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im.load()
|
||||
|
||||
# Assert that the error code is IMAGING_CODEC_MEMORY
|
||||
assert str(e.value) == "-9"
|
||||
assert str(e.value) == "decoder error -9"
|
||||
|
||||
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
|
||||
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
|
||||
|
|
|
@ -618,7 +618,7 @@ class TestFilePng:
|
|||
with Image.open("Tests/images/truncated_image.png") as im:
|
||||
# The file is truncated
|
||||
with pytest.raises(OSError):
|
||||
im.text()
|
||||
im.text
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
assert isinstance(im.text, dict)
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
|
|
@ -7,7 +7,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageSequence, SpiderImagePlugin
|
||||
from PIL import Image, SpiderImagePlugin
|
||||
|
||||
from .helper import assert_image_equal, hopper, is_pypy
|
||||
|
||||
|
@ -153,8 +153,8 @@ def test_nonstack_file() -> None:
|
|||
|
||||
def test_nonstack_dos() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
for i, frame in enumerate(ImageSequence.Iterator(im)):
|
||||
assert i <= 1, "Non-stack DOS file test failed"
|
||||
with pytest.raises(EOFError):
|
||||
im.seek(0)
|
||||
|
||||
|
||||
# for issue #4093
|
||||
|
|
|
@ -115,6 +115,19 @@ class TestFileTiff:
|
|||
outfile = str(tmp_path / "temp.tif")
|
||||
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
||||
|
||||
def test_bigtiff_save(self, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im = hopper()
|
||||
im.save(outfile, big_tiff=True)
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
assert reloaded.tag_v2._bigtiff is True
|
||||
|
||||
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
assert reloaded.tag_v2._bigtiff is True
|
||||
|
||||
def test_seek_too_large(self) -> None:
|
||||
with pytest.raises(ValueError, match="Unable to seek to frame"):
|
||||
Image.open("Tests/images/seek_too_large.tif")
|
||||
|
@ -733,7 +746,7 @@ class TestFileTiff:
|
|||
assert reread.n_frames == 3
|
||||
|
||||
def test_fixoffsets(self) -> None:
|
||||
b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00")
|
||||
b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
|
||||
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||
b.seek(0)
|
||||
a.fixOffsets(1, isShort=True)
|
||||
|
@ -746,6 +759,37 @@ class TestFileTiff:
|
|||
with pytest.raises(RuntimeError):
|
||||
a.fixOffsets(1)
|
||||
|
||||
b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
|
||||
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||
a.offsetOfNewPage = 2**16
|
||||
|
||||
b.seek(0)
|
||||
a.fixOffsets(1, isShort=True)
|
||||
|
||||
b = BytesIO(b"II\x2B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||
a.offsetOfNewPage = 2**32
|
||||
|
||||
b.seek(0)
|
||||
a.fixOffsets(1, isShort=True)
|
||||
|
||||
b.seek(0)
|
||||
a.fixOffsets(1, isLong=True)
|
||||
|
||||
def test_appending_tiff_writer_writelong(self) -> None:
|
||||
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
b = BytesIO(data)
|
||||
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||
a.writeLong(2**32 - 1)
|
||||
assert b.getvalue() == data + b"\xff\xff\xff\xff"
|
||||
|
||||
def test_appending_tiff_writer_rewritelastshorttolong(self) -> None:
|
||||
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
b = BytesIO(data)
|
||||
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||
a.rewriteLastShortToLong(2**32 - 1)
|
||||
assert b.getvalue() == data[:-2] + b"\xff\xff\xff\xff"
|
||||
|
||||
def test_saving_icc_profile(self, tmp_path: Path) -> None:
|
||||
# Tests saving TIFF with icc_profile set.
|
||||
# At the time of writing this will only work for non-compressed tiffs
|
||||
|
|
|
@ -189,8 +189,6 @@ class TestImage:
|
|||
if ext == ".jp2" and not features.check_codec("jpg_2000"):
|
||||
pytest.skip("jpg_2000 not available")
|
||||
temp_file = str(tmp_path / ("temp." + ext))
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
im.save(Path(temp_file))
|
||||
|
||||
def test_fp_name(self, tmp_path: Path) -> None:
|
||||
|
@ -667,7 +665,7 @@ class TestImage:
|
|||
# Test illegal image mode
|
||||
with hopper() as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.remap_palette(None)
|
||||
im.remap_palette([])
|
||||
|
||||
def test_remap_palette_transparency(self) -> None:
|
||||
im = Image.new("P", (1, 2), (0, 0, 0))
|
||||
|
@ -770,7 +768,7 @@ class TestImage:
|
|||
assert dict(exif)
|
||||
|
||||
# Test that exif data is cleared after another load
|
||||
exif.load(None)
|
||||
exif.load(b"")
|
||||
assert not dict(exif)
|
||||
|
||||
# Test loading just the EXIF header
|
||||
|
@ -991,6 +989,11 @@ class TestImage:
|
|||
else:
|
||||
assert im.getxmp() == {"xmpmeta": None}
|
||||
|
||||
def test_get_child_images(self) -> None:
|
||||
im = Image.new("RGB", (1, 1))
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert im.get_child_images() == []
|
||||
|
||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
|
||||
im = Image.new("RGB", size)
|
||||
|
|
|
@ -271,13 +271,25 @@ class TestImagePutPixelError:
|
|||
|
||||
|
||||
class TestEmbeddable:
|
||||
@pytest.mark.xfail(reason="failing test")
|
||||
@pytest.mark.xfail(not (sys.version_info >= (3, 13)), reason="failing test")
|
||||
@pytest.mark.skipif(not is_win32(), reason="requires Windows")
|
||||
def test_embeddable(self) -> None:
|
||||
import ctypes
|
||||
|
||||
from setuptools.command import build_ext
|
||||
|
||||
compiler = getattr(build_ext, "new_compiler")()
|
||||
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
|
||||
|
||||
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
|
||||
"INCLUDEPY"
|
||||
).replace("include", "libs")
|
||||
compiler.add_library_dir(libdir)
|
||||
try:
|
||||
compiler.initialize()
|
||||
except Exception:
|
||||
pytest.skip("Compiler could not be initialized")
|
||||
|
||||
with open("embed_pil.c", "w", encoding="utf-8") as fh:
|
||||
home = sys.prefix.replace("\\", "\\\\")
|
||||
fh.write(
|
||||
|
@ -305,13 +317,6 @@ int main(int argc, char* argv[])
|
|||
"""
|
||||
)
|
||||
|
||||
compiler = getattr(build_ext, "new_compiler")()
|
||||
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
|
||||
|
||||
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
|
||||
"INCLUDEPY"
|
||||
).replace("include", "libs")
|
||||
compiler.add_library_dir(libdir)
|
||||
objects = compiler.compile(["embed_pil.c"])
|
||||
compiler.link_executable(objects, "embed_pil")
|
||||
|
||||
|
|
|
@ -309,7 +309,7 @@ class TestImageResize:
|
|||
# Test unknown resampling filter
|
||||
with hopper() as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.resize((10, 10), "unknown")
|
||||
im.resize((10, 10), -1)
|
||||
|
||||
@skip_unless_feature("libtiff")
|
||||
def test_transposed(self) -> None:
|
||||
|
|
|
@ -405,7 +405,6 @@ def test_exif_transpose() -> None:
|
|||
else:
|
||||
original_exif = im.info["exif"]
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert_image_similar(base_im, transposed_im, 17)
|
||||
if orientation_im is base_im:
|
||||
assert "exif" not in im.info
|
||||
|
@ -417,7 +416,6 @@ def test_exif_transpose() -> None:
|
|||
|
||||
# Repeat the operation to test that it does not keep transposing
|
||||
transposed_im2 = ImageOps.exif_transpose(transposed_im)
|
||||
assert transposed_im2 is not None
|
||||
assert_image_equal(transposed_im2, transposed_im)
|
||||
|
||||
check(base_im)
|
||||
|
@ -433,7 +431,6 @@ def test_exif_transpose() -> None:
|
|||
assert im.getexif()[0x0112] == 3
|
||||
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
transposed_im._reload_exif()
|
||||
|
@ -446,14 +443,12 @@ def test_exif_transpose() -> None:
|
|||
assert im.getexif()[0x0112] == 3
|
||||
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
# Orientation set directly on Image.Exif
|
||||
im = hopper()
|
||||
im.getexif()[0x0112] = 3
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
|
||||
|
@ -464,7 +459,6 @@ def test_exif_transpose_xml_without_xmp() -> None:
|
|||
|
||||
del im.info["xmp"]
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
|
||||
|
|
|
@ -6,12 +6,11 @@ Goals
|
|||
|
||||
The fork author's goal is to foster and support active development of PIL through:
|
||||
|
||||
- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_
|
||||
- Continuous integration testing via `GitHub Actions`_
|
||||
- Publicized development activity on `GitHub`_
|
||||
- Regular releases to the `Python Package Index`_
|
||||
|
||||
.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
|
||||
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
|
||||
.. _GitHub: https://github.com/python-pillow/Pillow
|
||||
.. _Python Package Index: https://pypi.org/project/pillow/
|
||||
|
||||
|
|
|
@ -175,6 +175,24 @@ deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obt
|
|||
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
|
||||
``Image.Image.getim()``, which returns a ``Capsule`` object.
|
||||
|
||||
ExifTags.IFD.Makernote
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 11.1.0
|
||||
|
||||
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
|
||||
``ExifTags.IFD.MakerNote``.
|
||||
|
||||
Image.Image.get_child_images()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 11.2.0
|
||||
|
||||
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
|
||||
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
|
||||
method uses an image's file pointer, and so child images could only be retrieved from
|
||||
an :py:class:`PIL.ImageFile.ImageFile` instance.
|
||||
|
||||
Removed features
|
||||
----------------
|
||||
|
||||
|
|
|
@ -1208,6 +1208,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
|
|||
|
||||
.. versionadded:: 8.4.0
|
||||
|
||||
**big_tiff**
|
||||
If true, the image will be saved as a BigTIFF.
|
||||
|
||||
.. versionadded:: 11.1.0
|
||||
|
||||
**compression**
|
||||
A string containing the desired compression method for the
|
||||
file. (valid only with libtiff installed) Valid compression
|
||||
|
|
|
@ -33,10 +33,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
|
|||
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml
|
||||
:alt: GitHub Actions build status (Test Cygwin)
|
||||
|
||||
.. image:: https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build
|
||||
:target: https://ci.appveyor.com/project/python-pillow/Pillow
|
||||
:alt: AppVeyor CI build status (Windows)
|
||||
|
||||
.. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
|
||||
:target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
|
||||
:alt: GitHub Actions build status (Wheels)
|
||||
|
|
|
@ -27,6 +27,8 @@ These platforms are built and tested for every change.
|
|||
+----------------------------------+----------------------------+---------------------+
|
||||
| CentOS Stream 9 | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| CentOS Stream 10 | 3.12 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 40 | 3.12 | x86-64 |
|
||||
|
@ -42,18 +44,14 @@ These platforms are built and tested for every change.
|
|||
+----------------------------------+----------------------------+---------------------+
|
||||
| Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 |
|
||||
| | 3.12, 3.13, PyPy3 | |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.10 | arm64v8 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
|
||||
| | | s390x |
|
||||
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, |
|
||||
| | | ppc64le, s390x |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Windows Server 2019 | 3.9 | x86-64 |
|
||||
| Windows Server 2019 | 3.9 | x86 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 |
|
||||
| | 3.12, 3.13, PyPy3 | |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.13 | x86 |
|
||||
| Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 |
|
||||
| | PyPy3 | |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.12 (MinGW) | x86-64 |
|
||||
| +----------------------------+---------------------+
|
||||
|
@ -75,7 +73,7 @@ These platforms have been reported to work at the versions mentioned.
|
|||
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||
| | | versions | | Pillow version | | processors |
|
||||
+==================================+============================+==================+==============+
|
||||
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm |
|
||||
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0 |arm |
|
||||
| +----------------------------+------------------+ |
|
||||
| | 3.8 | 10.4.0 | |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
|
|
|
@ -54,6 +54,7 @@ Feature version numbers are available only where stated.
|
|||
Support for the following features can be checked:
|
||||
|
||||
* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available.
|
||||
* ``mozjpeg``: (compile time) Whether Pillow was compiled against the MozJPEG version of libjpeg. Compile-time version number is available.
|
||||
* ``zlib_ng``: (compile time) Whether Pillow was compiled against the zlib-ng version of zlib. Compile-time version number is available.
|
||||
* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer.
|
||||
* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available.
|
||||
|
|
|
@ -1,32 +1,14 @@
|
|||
11.1.0
|
||||
------
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
:cve:`YYYY-XXXXX`: TODO
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Backwards Incompatible Changes
|
||||
==============================
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
ExifTags.IFD.Makernote
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
|
||||
``ExifTags.IFD.MakerNote``.
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
@ -65,6 +47,13 @@ zlib library, and what version of zlib-ng is being used::
|
|||
features.check_feature("zlib_ng") # True or False
|
||||
features.version_feature("zlib_ng") # "2.2.2" for example, or None
|
||||
|
||||
Saving TIFF as BigTIFF
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument::
|
||||
|
||||
im.save("out.tiff", big_tiff=True)
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
|
@ -79,6 +68,11 @@ Saving JPEG 2000 CMYK images
|
|||
|
||||
With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files.
|
||||
|
||||
Minimum C version
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
C99 is now the minimum version of C required to compile Pillow from source.
|
||||
|
||||
zlib-ng in wheels
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
63
docs/releasenotes/11.2.0.rst
Normal file
63
docs/releasenotes/11.2.0.rst
Normal file
|
@ -0,0 +1,63 @@
|
|||
11.2.0
|
||||
------
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
:cve:`YYYY-XXXXX`: TODO
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Backwards Incompatible Changes
|
||||
==============================
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
||||
Image.Image.get_child_images()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 11.2.0
|
||||
|
||||
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
|
||||
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
|
||||
method uses an image's file pointer, and so child images could only be retrieved from
|
||||
an :py:class:`PIL.ImageFile.ImageFile` instance.
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
API Additions
|
||||
=============
|
||||
|
||||
Check for MozJPEG
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can check if Pillow has been built against the MozJPEG version of the
|
||||
libjpeg library, and what version of MozJPEG is being used::
|
||||
|
||||
from PIL import features
|
||||
features.check_feature("mozjpeg") # True or False
|
||||
features.version_feature("mozjpeg") # "4.1.1" for example, or None
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
|
@ -14,6 +14,7 @@ expected to be backported to earlier versions.
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
11.2.0
|
||||
11.1.0
|
||||
11.0.0
|
||||
10.4.0
|
||||
|
|
|
@ -259,21 +259,36 @@ class BlpImageFile(ImageFile.ImageFile):
|
|||
|
||||
def _open(self) -> None:
|
||||
self.magic = self.fp.read(4)
|
||||
|
||||
self.fp.seek(5, os.SEEK_CUR)
|
||||
(self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
|
||||
|
||||
self.fp.seek(2, os.SEEK_CUR)
|
||||
self._size = struct.unpack("<II", self.fp.read(8))
|
||||
|
||||
if self.magic in (b"BLP1", b"BLP2"):
|
||||
decoder = self.magic.decode()
|
||||
else:
|
||||
if not _accept(self.magic):
|
||||
msg = f"Bad BLP magic {repr(self.magic)}"
|
||||
raise BLPFormatError(msg)
|
||||
|
||||
self._mode = "RGBA" if self._blp_alpha_depth else "RGB"
|
||||
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, self.mode)]
|
||||
compression = struct.unpack("<i", self.fp.read(4))[0]
|
||||
if self.magic == b"BLP1":
|
||||
alpha = struct.unpack("<I", self.fp.read(4))[0] != 0
|
||||
else:
|
||||
encoding = struct.unpack("<b", self.fp.read(1))[0]
|
||||
alpha = struct.unpack("<b", self.fp.read(1))[0] != 0
|
||||
alpha_encoding = struct.unpack("<b", self.fp.read(1))[0]
|
||||
self.fp.seek(1, os.SEEK_CUR) # mips
|
||||
|
||||
self._size = struct.unpack("<II", self.fp.read(8))
|
||||
|
||||
args: tuple[int, int, bool] | tuple[int, int, bool, int]
|
||||
if self.magic == b"BLP1":
|
||||
encoding = struct.unpack("<i", self.fp.read(4))[0]
|
||||
self.fp.seek(4, os.SEEK_CUR) # subtype
|
||||
|
||||
args = (compression, encoding, alpha)
|
||||
offset = 28
|
||||
else:
|
||||
args = (compression, encoding, alpha, alpha_encoding)
|
||||
offset = 20
|
||||
|
||||
decoder = self.magic.decode()
|
||||
|
||||
self._mode = "RGBA" if alpha else "RGB"
|
||||
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]
|
||||
|
||||
|
||||
class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||
|
@ -281,7 +296,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
|||
|
||||
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
||||
try:
|
||||
self._read_blp_header()
|
||||
self._read_header()
|
||||
self._load()
|
||||
except struct.error as e:
|
||||
msg = "Truncated BLP file"
|
||||
|
@ -292,25 +307,9 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
|||
def _load(self) -> None:
|
||||
pass
|
||||
|
||||
def _read_blp_header(self) -> None:
|
||||
assert self.fd is not None
|
||||
self.fd.seek(4)
|
||||
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
|
||||
|
||||
(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1))
|
||||
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1))
|
||||
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1))
|
||||
self.fd.seek(1, os.SEEK_CUR) # mips
|
||||
|
||||
self.size = struct.unpack("<II", self._safe_read(8))
|
||||
|
||||
if isinstance(self, BLP1Decoder):
|
||||
# Only present for BLP1
|
||||
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
|
||||
self.fd.seek(4, os.SEEK_CUR) # subtype
|
||||
|
||||
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
|
||||
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
|
||||
def _read_header(self) -> None:
|
||||
self._offsets = struct.unpack("<16I", self._safe_read(16 * 4))
|
||||
self._lengths = struct.unpack("<16I", self._safe_read(16 * 4))
|
||||
|
||||
def _safe_read(self, length: int) -> bytes:
|
||||
assert self.fd is not None
|
||||
|
@ -326,9 +325,11 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
|||
ret.append((b, g, r, a))
|
||||
return ret
|
||||
|
||||
def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray:
|
||||
def _read_bgra(
|
||||
self, palette: list[tuple[int, int, int, int]], alpha: bool
|
||||
) -> bytearray:
|
||||
data = bytearray()
|
||||
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
|
||||
_data = BytesIO(self._safe_read(self._lengths[0]))
|
||||
while True:
|
||||
try:
|
||||
(offset,) = struct.unpack("<B", _data.read(1))
|
||||
|
@ -336,7 +337,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
|||
break
|
||||
b, g, r, a = palette[offset]
|
||||
d: tuple[int, ...] = (r, g, b)
|
||||
if self._blp_alpha_depth:
|
||||
if alpha:
|
||||
d += (a,)
|
||||
data.extend(d)
|
||||
return data
|
||||
|
@ -344,19 +345,21 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
|||
|
||||
class BLP1Decoder(_BLPBaseDecoder):
|
||||
def _load(self) -> None:
|
||||
if self._blp_compression == Format.JPEG:
|
||||
self._compression, self._encoding, alpha = self.args
|
||||
|
||||
if self._compression == Format.JPEG:
|
||||
self._decode_jpeg_stream()
|
||||
|
||||
elif self._blp_compression == 1:
|
||||
if self._blp_encoding in (4, 5):
|
||||
elif self._compression == 1:
|
||||
if self._encoding in (4, 5):
|
||||
palette = self._read_palette()
|
||||
data = self._read_bgra(palette)
|
||||
data = self._read_bgra(palette, alpha)
|
||||
self.set_as_raw(data)
|
||||
else:
|
||||
msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}"
|
||||
msg = f"Unsupported BLP encoding {repr(self._encoding)}"
|
||||
raise BLPFormatError(msg)
|
||||
else:
|
||||
msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
|
||||
msg = f"Unsupported BLP compression {repr(self._encoding)}"
|
||||
raise BLPFormatError(msg)
|
||||
|
||||
def _decode_jpeg_stream(self) -> None:
|
||||
|
@ -365,65 +368,61 @@ class BLP1Decoder(_BLPBaseDecoder):
|
|||
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
|
||||
jpeg_header = self._safe_read(jpeg_header_size)
|
||||
assert self.fd is not None
|
||||
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
|
||||
data = self._safe_read(self._blp_lengths[0])
|
||||
self._safe_read(self._offsets[0] - self.fd.tell()) # What IS this?
|
||||
data = self._safe_read(self._lengths[0])
|
||||
data = jpeg_header + data
|
||||
image = JpegImageFile(BytesIO(data))
|
||||
Image._decompression_bomb_check(image.size)
|
||||
if image.mode == "CMYK":
|
||||
decoder_name, extents, offset, args = image.tile[0]
|
||||
args = image.tile[0].args
|
||||
assert isinstance(args, tuple)
|
||||
image.tile = [
|
||||
ImageFile._Tile(decoder_name, extents, offset, (args[0], "CMYK"))
|
||||
]
|
||||
r, g, b = image.convert("RGB").split()
|
||||
reversed_image = Image.merge("RGB", (b, g, r))
|
||||
self.set_as_raw(reversed_image.tobytes())
|
||||
image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))]
|
||||
self.set_as_raw(image.convert("RGB").tobytes(), "BGR")
|
||||
|
||||
|
||||
class BLP2Decoder(_BLPBaseDecoder):
|
||||
def _load(self) -> None:
|
||||
self._compression, self._encoding, alpha, self._alpha_encoding = self.args
|
||||
|
||||
palette = self._read_palette()
|
||||
|
||||
assert self.fd is not None
|
||||
self.fd.seek(self._blp_offsets[0])
|
||||
self.fd.seek(self._offsets[0])
|
||||
|
||||
if self._blp_compression == 1:
|
||||
if self._compression == 1:
|
||||
# Uncompressed or DirectX compression
|
||||
|
||||
if self._blp_encoding == Encoding.UNCOMPRESSED:
|
||||
data = self._read_bgra(palette)
|
||||
if self._encoding == Encoding.UNCOMPRESSED:
|
||||
data = self._read_bgra(palette, alpha)
|
||||
|
||||
elif self._blp_encoding == Encoding.DXT:
|
||||
elif self._encoding == Encoding.DXT:
|
||||
data = bytearray()
|
||||
if self._blp_alpha_encoding == AlphaEncoding.DXT1:
|
||||
linesize = (self.size[0] + 3) // 4 * 8
|
||||
for yb in range((self.size[1] + 3) // 4):
|
||||
for d in decode_dxt1(
|
||||
self._safe_read(linesize), alpha=bool(self._blp_alpha_depth)
|
||||
):
|
||||
if self._alpha_encoding == AlphaEncoding.DXT1:
|
||||
linesize = (self.state.xsize + 3) // 4 * 8
|
||||
for yb in range((self.state.ysize + 3) // 4):
|
||||
for d in decode_dxt1(self._safe_read(linesize), alpha):
|
||||
data += d
|
||||
|
||||
elif self._blp_alpha_encoding == AlphaEncoding.DXT3:
|
||||
linesize = (self.size[0] + 3) // 4 * 16
|
||||
for yb in range((self.size[1] + 3) // 4):
|
||||
elif self._alpha_encoding == AlphaEncoding.DXT3:
|
||||
linesize = (self.state.xsize + 3) // 4 * 16
|
||||
for yb in range((self.state.ysize + 3) // 4):
|
||||
for d in decode_dxt3(self._safe_read(linesize)):
|
||||
data += d
|
||||
|
||||
elif self._blp_alpha_encoding == AlphaEncoding.DXT5:
|
||||
linesize = (self.size[0] + 3) // 4 * 16
|
||||
for yb in range((self.size[1] + 3) // 4):
|
||||
elif self._alpha_encoding == AlphaEncoding.DXT5:
|
||||
linesize = (self.state.xsize + 3) // 4 * 16
|
||||
for yb in range((self.state.ysize + 3) // 4):
|
||||
for d in decode_dxt5(self._safe_read(linesize)):
|
||||
data += d
|
||||
else:
|
||||
msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}"
|
||||
msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}"
|
||||
raise BLPFormatError(msg)
|
||||
else:
|
||||
msg = f"Unknown BLP encoding {repr(self._blp_encoding)}"
|
||||
msg = f"Unknown BLP encoding {repr(self._encoding)}"
|
||||
raise BLPFormatError(msg)
|
||||
|
||||
else:
|
||||
msg = f"Unknown BLP compression {repr(self._blp_compression)}"
|
||||
msg = f"Unknown BLP compression {repr(self._compression)}"
|
||||
raise BLPFormatError(msg)
|
||||
|
||||
self.set_as_raw(data)
|
||||
|
@ -472,10 +471,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
|||
|
||||
assert im.palette is not None
|
||||
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
|
||||
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
|
||||
fp.write(struct.pack("<b", 1 if im.palette.mode == "RGBA" else 0))
|
||||
fp.write(struct.pack("<b", 0)) # alpha encoding
|
||||
fp.write(struct.pack("<b", 0)) # mips
|
||||
|
||||
alpha_depth = 1 if im.palette.mode == "RGBA" else 0
|
||||
if magic == b"BLP1":
|
||||
fp.write(struct.pack("<L", alpha_depth))
|
||||
else:
|
||||
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
|
||||
fp.write(struct.pack("<b", alpha_depth))
|
||||
fp.write(struct.pack("<b", 0)) # alpha encoding
|
||||
fp.write(struct.pack("<b", 0)) # mips
|
||||
fp.write(struct.pack("<II", *im.size))
|
||||
if magic == b"BLP1":
|
||||
fp.write(struct.pack("<i", 5))
|
||||
|
|
|
@ -353,6 +353,7 @@ class IFD(IntEnum):
|
|||
Exif = 0x8769
|
||||
GPSInfo = 0x8825
|
||||
MakerNote = 0x927C
|
||||
Makernote = 0x927C # Deprecated
|
||||
Interop = 0xA005
|
||||
IFD1 = -1
|
||||
|
||||
|
|
|
@ -603,24 +603,16 @@ class Image:
|
|||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def _close_fp(self):
|
||||
if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError):
|
||||
if self._fp != self.fp:
|
||||
self._fp.close()
|
||||
self._fp = DeferredError(ValueError("Operation on closed image"))
|
||||
if self.fp:
|
||||
self.fp.close()
|
||||
|
||||
def __exit__(self, *args):
|
||||
if hasattr(self, "fp"):
|
||||
from . import ImageFile
|
||||
|
||||
if isinstance(self, ImageFile.ImageFile):
|
||||
if getattr(self, "_exclusive_fp", False):
|
||||
self._close_fp()
|
||||
self.fp = None
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Closes the file pointer, if possible.
|
||||
|
||||
This operation will destroy the image core and release its memory.
|
||||
The image data will be unusable afterward.
|
||||
|
||||
|
@ -629,13 +621,6 @@ class Image:
|
|||
:py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for
|
||||
more information.
|
||||
"""
|
||||
if hasattr(self, "fp"):
|
||||
try:
|
||||
self._close_fp()
|
||||
self.fp = None
|
||||
except Exception as msg:
|
||||
logger.debug("Error closing: %s", msg)
|
||||
|
||||
if getattr(self, "map", None):
|
||||
self.map: mmap.mmap | None = None
|
||||
|
||||
|
@ -1554,50 +1539,10 @@ class Image:
|
|||
self.getexif()
|
||||
|
||||
def get_child_images(self) -> list[ImageFile.ImageFile]:
|
||||
child_images = []
|
||||
exif = self.getexif()
|
||||
ifds = []
|
||||
if ExifTags.Base.SubIFDs in exif:
|
||||
subifd_offsets = exif[ExifTags.Base.SubIFDs]
|
||||
if subifd_offsets:
|
||||
if not isinstance(subifd_offsets, tuple):
|
||||
subifd_offsets = (subifd_offsets,)
|
||||
for subifd_offset in subifd_offsets:
|
||||
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
|
||||
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
|
||||
if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset):
|
||||
assert exif._info is not None
|
||||
ifds.append((ifd1, exif._info.next))
|
||||
from . import ImageFile
|
||||
|
||||
offset = None
|
||||
for ifd, ifd_offset in ifds:
|
||||
current_offset = self.fp.tell()
|
||||
if offset is None:
|
||||
offset = current_offset
|
||||
|
||||
fp = self.fp
|
||||
if ifd is not None:
|
||||
thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset)
|
||||
if thumbnail_offset is not None:
|
||||
thumbnail_offset += getattr(self, "_exif_offset", 0)
|
||||
self.fp.seek(thumbnail_offset)
|
||||
data = self.fp.read(ifd.get(ExifTags.Base.JpegIFByteCount))
|
||||
fp = io.BytesIO(data)
|
||||
|
||||
with open(fp) as im:
|
||||
from . import TiffImagePlugin
|
||||
|
||||
if thumbnail_offset is None and isinstance(
|
||||
im, TiffImagePlugin.TiffImageFile
|
||||
):
|
||||
im._frame_pos = [ifd_offset]
|
||||
im._seek(0)
|
||||
im.load()
|
||||
child_images.append(im)
|
||||
|
||||
if offset is not None:
|
||||
self.fp.seek(offset)
|
||||
return child_images
|
||||
deprecate("Image.Image.get_child_images", 13)
|
||||
return ImageFile.ImageFile.get_child_images(self) # type: ignore[arg-type]
|
||||
|
||||
def getim(self) -> CapsuleType:
|
||||
"""
|
||||
|
|
|
@ -31,18 +31,21 @@ from __future__ import annotations
|
|||
import abc
|
||||
import io
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
|
||||
|
||||
from . import Image
|
||||
from . import ExifTags, Image
|
||||
from ._deprecate import deprecate
|
||||
from ._util import is_path
|
||||
from ._util import DeferredError, is_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._typing import StrOrBytesPath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAXBLOCK = 65536
|
||||
|
||||
SAFEBLOCK = 1024 * 1024
|
||||
|
@ -163,6 +166,85 @@ class ImageFile(Image.Image):
|
|||
def _open(self) -> None:
|
||||
pass
|
||||
|
||||
def _close_fp(self):
|
||||
if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError):
|
||||
if self._fp != self.fp:
|
||||
self._fp.close()
|
||||
self._fp = DeferredError(ValueError("Operation on closed image"))
|
||||
if self.fp:
|
||||
self.fp.close()
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Closes the file pointer, if possible.
|
||||
|
||||
This operation will destroy the image core and release its memory.
|
||||
The image data will be unusable afterward.
|
||||
|
||||
This function is required to close images that have multiple frames or
|
||||
have not had their file read and closed by the
|
||||
:py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for
|
||||
more information.
|
||||
"""
|
||||
try:
|
||||
self._close_fp()
|
||||
self.fp = None
|
||||
except Exception as msg:
|
||||
logger.debug("Error closing: %s", msg)
|
||||
|
||||
super().close()
|
||||
|
||||
def get_child_images(self) -> list[ImageFile]:
|
||||
child_images = []
|
||||
exif = self.getexif()
|
||||
ifds = []
|
||||
if ExifTags.Base.SubIFDs in exif:
|
||||
subifd_offsets = exif[ExifTags.Base.SubIFDs]
|
||||
if subifd_offsets:
|
||||
if not isinstance(subifd_offsets, tuple):
|
||||
subifd_offsets = (subifd_offsets,)
|
||||
for subifd_offset in subifd_offsets:
|
||||
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
|
||||
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
|
||||
if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset):
|
||||
assert exif._info is not None
|
||||
ifds.append((ifd1, exif._info.next))
|
||||
|
||||
offset = None
|
||||
for ifd, ifd_offset in ifds:
|
||||
assert self.fp is not None
|
||||
current_offset = self.fp.tell()
|
||||
if offset is None:
|
||||
offset = current_offset
|
||||
|
||||
fp = self.fp
|
||||
if ifd is not None:
|
||||
thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset)
|
||||
if thumbnail_offset is not None:
|
||||
thumbnail_offset += getattr(self, "_exif_offset", 0)
|
||||
self.fp.seek(thumbnail_offset)
|
||||
|
||||
length = ifd.get(ExifTags.Base.JpegIFByteCount)
|
||||
assert isinstance(length, int)
|
||||
data = self.fp.read(length)
|
||||
fp = io.BytesIO(data)
|
||||
|
||||
with Image.open(fp) as im:
|
||||
from . import TiffImagePlugin
|
||||
|
||||
if thumbnail_offset is None and isinstance(
|
||||
im, TiffImagePlugin.TiffImageFile
|
||||
):
|
||||
im._frame_pos = [ifd_offset]
|
||||
im._seek(0)
|
||||
im.load()
|
||||
child_images.append(im)
|
||||
|
||||
if offset is not None:
|
||||
assert self.fp is not None
|
||||
self.fp.seek(offset)
|
||||
return child_images
|
||||
|
||||
def get_format_mimetype(self) -> str | None:
|
||||
if self.custom_mimetype:
|
||||
return self.custom_mimetype
|
||||
|
|
|
@ -22,7 +22,7 @@ import functools
|
|||
import operator
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from typing import Protocol, cast
|
||||
from typing import Literal, Protocol, cast, overload
|
||||
|
||||
from . import ExifTags, Image, ImagePalette
|
||||
|
||||
|
@ -673,6 +673,16 @@ def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
|
|||
return _lut(image, lut)
|
||||
|
||||
|
||||
@overload
|
||||
def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ...
|
||||
|
||||
|
||||
@overload
|
||||
def exif_transpose(
|
||||
image: Image.Image, *, in_place: Literal[False] = False
|
||||
) -> Image.Image: ...
|
||||
|
||||
|
||||
def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
|
||||
"""
|
||||
If an image has an EXIF Orientation tag, other than 1, transpose the image
|
||||
|
|
|
@ -270,7 +270,7 @@ def makeSpiderHeader(im: Image.Image) -> list[bytes]:
|
|||
|
||||
|
||||
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||
if im.mode[0] != "F":
|
||||
if im.mode != "F":
|
||||
im = im.convert("F")
|
||||
|
||||
hdr = makeSpiderHeader(im)
|
||||
|
|
|
@ -582,7 +582,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
ifh: bytes = b"II\052\0\0\0\0\0",
|
||||
ifh: bytes = b"II\x2A\x00\x00\x00\x00\x00",
|
||||
prefix: bytes | None = None,
|
||||
group: int | None = None,
|
||||
) -> None:
|
||||
|
@ -949,12 +949,25 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
|||
warnings.warn(str(msg))
|
||||
return
|
||||
|
||||
def _get_ifh(self) -> bytes:
|
||||
ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42)
|
||||
if self._bigtiff:
|
||||
ifh += self._pack("HH", 8, 0)
|
||||
ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8)
|
||||
|
||||
return ifh
|
||||
|
||||
def tobytes(self, offset: int = 0) -> bytes:
|
||||
# FIXME What about tagdata?
|
||||
result = self._pack("H", len(self._tags_v2))
|
||||
result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
|
||||
|
||||
entries: list[tuple[int, int, int, bytes, bytes]] = []
|
||||
offset = offset + len(result) + len(self._tags_v2) * 12 + 4
|
||||
|
||||
fmt = "Q" if self._bigtiff else "L"
|
||||
fmt_size = 8 if self._bigtiff else 4
|
||||
offset += (
|
||||
len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size
|
||||
)
|
||||
stripoffsets = None
|
||||
|
||||
# pass 1: convert tags to binary format
|
||||
|
@ -966,11 +979,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
|||
logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
|
||||
is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
|
||||
if is_ifd:
|
||||
if self._endian == "<":
|
||||
ifh = b"II\x2A\x00\x08\x00\x00\x00"
|
||||
else:
|
||||
ifh = b"MM\x00\x2A\x00\x00\x00\x08"
|
||||
ifd = ImageFileDirectory_v2(ifh, group=tag)
|
||||
ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag)
|
||||
values = self._tags_v2[tag]
|
||||
for ifd_tag, ifd_value in values.items():
|
||||
ifd[ifd_tag] = ifd_value
|
||||
|
@ -993,10 +1002,10 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
|||
else:
|
||||
count = len(values)
|
||||
# figure out if data fits into the entry
|
||||
if len(data) <= 4:
|
||||
entries.append((tag, typ, count, data.ljust(4, b"\0"), b""))
|
||||
if len(data) <= fmt_size:
|
||||
entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b""))
|
||||
else:
|
||||
entries.append((tag, typ, count, self._pack("L", offset), data))
|
||||
entries.append((tag, typ, count, self._pack(fmt, offset), data))
|
||||
offset += (len(data) + 1) // 2 * 2 # pad to word
|
||||
|
||||
# update strip offset data to point beyond auxiliary data
|
||||
|
@ -1007,16 +1016,18 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
|||
values = [val + offset for val in handler(self, data, self.legacy_api)]
|
||||
data = self._write_dispatch[typ](self, *values)
|
||||
else:
|
||||
value = self._pack("L", self._unpack("L", value)[0] + offset)
|
||||
value = self._pack(fmt, self._unpack(fmt, value)[0] + offset)
|
||||
entries[stripoffsets] = tag, typ, count, value, data
|
||||
|
||||
# pass 2: write entries to file
|
||||
for tag, typ, count, value, data in entries:
|
||||
logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data))
|
||||
result += self._pack("HHL4s", tag, typ, count, value)
|
||||
result += self._pack(
|
||||
"HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value
|
||||
)
|
||||
|
||||
# -- overwrite here for multi-page --
|
||||
result += b"\0\0\0\0" # end of entries
|
||||
result += self._pack(fmt, 0) # end of entries
|
||||
|
||||
# pass 3: write auxiliary data to file
|
||||
for tag, typ, count, value, data in entries:
|
||||
|
@ -1028,8 +1039,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
|||
|
||||
def save(self, fp: IO[bytes]) -> int:
|
||||
if fp.tell() == 0: # skip TIFF header on subsequent pages
|
||||
# tiff header -- PIL always starts the first IFD at offset 8
|
||||
fp.write(self._prefix + self._pack("HL", 42, 8))
|
||||
fp.write(self._get_ifh())
|
||||
|
||||
offset = fp.tell()
|
||||
result = self.tobytes(offset)
|
||||
|
@ -1401,7 +1411,8 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
self.fp = None # might be shared
|
||||
|
||||
if err < 0:
|
||||
raise OSError(err)
|
||||
msg = f"decoder error {err}"
|
||||
raise OSError(msg)
|
||||
|
||||
return Image.Image.load(self)
|
||||
|
||||
|
@ -1561,17 +1572,6 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
# fillorder==2 modes have a corresponding
|
||||
# fillorder=1 mode
|
||||
self._mode, rawmode = OPEN_INFO[key]
|
||||
# libtiff always returns the bytes in native order.
|
||||
# we're expecting image byte order. So, if the rawmode
|
||||
# contains I;16, we need to convert from native to image
|
||||
# byte order.
|
||||
if rawmode == "I;16":
|
||||
rawmode = "I;16N"
|
||||
if ";16B" in rawmode:
|
||||
rawmode = rawmode.replace(";16B", ";16N")
|
||||
if ";16L" in rawmode:
|
||||
rawmode = rawmode.replace(";16L", ";16N")
|
||||
|
||||
# YCbCr images with new jpeg compression with pixels in one plane
|
||||
# unpacked straight into RGB values
|
||||
if (
|
||||
|
@ -1580,6 +1580,14 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
and self._planar_configuration == 1
|
||||
):
|
||||
rawmode = "RGB"
|
||||
# libtiff always returns the bytes in native order.
|
||||
# we're expecting image byte order. So, if the rawmode
|
||||
# contains I;16, we need to convert from native to image
|
||||
# byte order.
|
||||
elif rawmode == "I;16":
|
||||
rawmode = "I;16N"
|
||||
elif rawmode.endswith(";16B") or rawmode.endswith(";16L"):
|
||||
rawmode = rawmode[:-1] + "N"
|
||||
|
||||
# Offset in the tile tuple is 0, we go from 0,0 to
|
||||
# w,h, and we only do this once -- eds
|
||||
|
@ -1685,10 +1693,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
|||
msg = f"cannot write mode {im.mode} as TIFF"
|
||||
raise OSError(msg) from e
|
||||
|
||||
ifd = ImageFileDirectory_v2(prefix=prefix)
|
||||
|
||||
encoderinfo = im.encoderinfo
|
||||
encoderconfig = im.encoderconfig
|
||||
|
||||
ifd = ImageFileDirectory_v2(prefix=prefix)
|
||||
if encoderinfo.get("big_tiff"):
|
||||
ifd._bigtiff = True
|
||||
|
||||
try:
|
||||
compression = encoderinfo["compression"]
|
||||
except KeyError:
|
||||
|
@ -2038,20 +2049,21 @@ class AppendingTiffWriter(io.BytesIO):
|
|||
self.offsetOfNewPage = 0
|
||||
|
||||
self.IIMM = iimm = self.f.read(4)
|
||||
self._bigtiff = b"\x2B" in iimm
|
||||
if not iimm:
|
||||
# empty file - first page
|
||||
self.isFirst = True
|
||||
return
|
||||
|
||||
self.isFirst = False
|
||||
if iimm == b"II\x2a\x00":
|
||||
self.setEndian("<")
|
||||
elif iimm == b"MM\x00\x2a":
|
||||
self.setEndian(">")
|
||||
else:
|
||||
if iimm not in PREFIXES:
|
||||
msg = "Invalid TIFF file header"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
self.setEndian("<" if iimm.startswith(II) else ">")
|
||||
|
||||
if self._bigtiff:
|
||||
self.f.seek(4, os.SEEK_CUR)
|
||||
self.skipIFDs()
|
||||
self.goToEnd()
|
||||
|
||||
|
@ -2071,11 +2083,13 @@ class AppendingTiffWriter(io.BytesIO):
|
|||
msg = "IIMM of new page doesn't match IIMM of first page"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
ifd_offset = self.readLong()
|
||||
if self._bigtiff:
|
||||
self.f.seek(4, os.SEEK_CUR)
|
||||
ifd_offset = self._read(8 if self._bigtiff else 4)
|
||||
ifd_offset += self.offsetOfNewPage
|
||||
assert self.whereToWriteNewIFDOffset is not None
|
||||
self.f.seek(self.whereToWriteNewIFDOffset)
|
||||
self.writeLong(ifd_offset)
|
||||
self._write(ifd_offset, 8 if self._bigtiff else 4)
|
||||
self.f.seek(ifd_offset)
|
||||
self.fixIFD()
|
||||
|
||||
|
@ -2121,18 +2135,20 @@ class AppendingTiffWriter(io.BytesIO):
|
|||
self.endian = endian
|
||||
self.longFmt = f"{self.endian}L"
|
||||
self.shortFmt = f"{self.endian}H"
|
||||
self.tagFormat = f"{self.endian}HHL"
|
||||
self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L")
|
||||
|
||||
def skipIFDs(self) -> None:
|
||||
while True:
|
||||
ifd_offset = self.readLong()
|
||||
ifd_offset = self._read(8 if self._bigtiff else 4)
|
||||
if ifd_offset == 0:
|
||||
self.whereToWriteNewIFDOffset = self.f.tell() - 4
|
||||
self.whereToWriteNewIFDOffset = self.f.tell() - (
|
||||
8 if self._bigtiff else 4
|
||||
)
|
||||
break
|
||||
|
||||
self.f.seek(ifd_offset)
|
||||
num_tags = self.readShort()
|
||||
self.f.seek(num_tags * 12, os.SEEK_CUR)
|
||||
num_tags = self._read(8 if self._bigtiff else 2)
|
||||
self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR)
|
||||
|
||||
def write(self, data: Buffer, /) -> int:
|
||||
return self.f.write(data)
|
||||
|
@ -2162,17 +2178,19 @@ class AppendingTiffWriter(io.BytesIO):
|
|||
msg = f"wrote only {bytes_written} bytes but wanted {expected}"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
def rewriteLastShortToLong(self, value: int) -> None:
|
||||
self.f.seek(-2, os.SEEK_CUR)
|
||||
bytes_written = self.f.write(struct.pack(self.longFmt, value))
|
||||
self._verify_bytes_written(bytes_written, 4)
|
||||
|
||||
def _rewriteLast(self, value: int, field_size: int) -> None:
|
||||
def _rewriteLast(
|
||||
self, value: int, field_size: int, new_field_size: int = 0
|
||||
) -> None:
|
||||
self.f.seek(-field_size, os.SEEK_CUR)
|
||||
if not new_field_size:
|
||||
new_field_size = field_size
|
||||
bytes_written = self.f.write(
|
||||
struct.pack(self.endian + self._fmt(field_size), value)
|
||||
struct.pack(self.endian + self._fmt(new_field_size), value)
|
||||
)
|
||||
self._verify_bytes_written(bytes_written, field_size)
|
||||
self._verify_bytes_written(bytes_written, new_field_size)
|
||||
|
||||
def rewriteLastShortToLong(self, value: int) -> None:
|
||||
self._rewriteLast(value, 2, 4)
|
||||
|
||||
def rewriteLastShort(self, value: int) -> None:
|
||||
return self._rewriteLast(value, 2)
|
||||
|
@ -2180,13 +2198,17 @@ class AppendingTiffWriter(io.BytesIO):
|
|||
def rewriteLastLong(self, value: int) -> None:
|
||||
return self._rewriteLast(value, 4)
|
||||
|
||||
def _write(self, value: int, field_size: int) -> None:
|
||||
bytes_written = self.f.write(
|
||||
struct.pack(self.endian + self._fmt(field_size), value)
|
||||
)
|
||||
self._verify_bytes_written(bytes_written, field_size)
|
||||
|
||||
def writeShort(self, value: int) -> None:
|
||||
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
|
||||
self._verify_bytes_written(bytes_written, 2)
|
||||
self._write(value, 2)
|
||||
|
||||
def writeLong(self, value: int) -> None:
|
||||
bytes_written = self.f.write(struct.pack(self.longFmt, value))
|
||||
self._verify_bytes_written(bytes_written, 4)
|
||||
self._write(value, 4)
|
||||
|
||||
def close(self) -> None:
|
||||
self.finalize()
|
||||
|
@ -2194,24 +2216,37 @@ class AppendingTiffWriter(io.BytesIO):
|
|||
self.f.close()
|
||||
|
||||
def fixIFD(self) -> None:
|
||||
num_tags = self.readShort()
|
||||
num_tags = self._read(8 if self._bigtiff else 2)
|
||||
|
||||
for i in range(num_tags):
|
||||
tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8))
|
||||
tag, field_type, count = struct.unpack(
|
||||
self.tagFormat, self.f.read(12 if self._bigtiff else 8)
|
||||
)
|
||||
|
||||
field_size = self.fieldSizes[field_type]
|
||||
total_size = field_size * count
|
||||
is_local = total_size <= 4
|
||||
fmt_size = 8 if self._bigtiff else 4
|
||||
is_local = total_size <= fmt_size
|
||||
if not is_local:
|
||||
offset = self.readLong() + self.offsetOfNewPage
|
||||
self.rewriteLastLong(offset)
|
||||
offset = self._read(fmt_size) + self.offsetOfNewPage
|
||||
self._rewriteLast(offset, fmt_size)
|
||||
|
||||
if tag in self.Tags:
|
||||
cur_pos = self.f.tell()
|
||||
|
||||
logger.debug(
|
||||
"fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d",
|
||||
TiffTags.lookup(tag).name,
|
||||
tag,
|
||||
TYPES.get(field_type, "unknown"),
|
||||
field_type,
|
||||
field_size,
|
||||
count,
|
||||
)
|
||||
|
||||
if is_local:
|
||||
self._fixOffsets(count, field_size)
|
||||
self.f.seek(cur_pos + 4)
|
||||
self.f.seek(cur_pos + fmt_size)
|
||||
else:
|
||||
self.f.seek(offset)
|
||||
self._fixOffsets(count, field_size)
|
||||
|
@ -2219,24 +2254,33 @@ class AppendingTiffWriter(io.BytesIO):
|
|||
|
||||
elif is_local:
|
||||
# skip the locally stored value that is not an offset
|
||||
self.f.seek(4, os.SEEK_CUR)
|
||||
self.f.seek(fmt_size, os.SEEK_CUR)
|
||||
|
||||
def _fixOffsets(self, count: int, field_size: int) -> None:
|
||||
for i in range(count):
|
||||
offset = self._read(field_size)
|
||||
offset += self.offsetOfNewPage
|
||||
if field_size == 2 and offset >= 65536:
|
||||
# offset is now too large - we must convert shorts to longs
|
||||
|
||||
new_field_size = 0
|
||||
if self._bigtiff and field_size in (2, 4) and offset >= 2**32:
|
||||
# offset is now too large - we must convert long to long8
|
||||
new_field_size = 8
|
||||
elif field_size == 2 and offset >= 2**16:
|
||||
# offset is now too large - we must convert short to long
|
||||
new_field_size = 4
|
||||
if new_field_size:
|
||||
if count != 1:
|
||||
msg = "not implemented"
|
||||
raise RuntimeError(msg) # XXX TODO
|
||||
|
||||
# simple case - the offset is just one and therefore it is
|
||||
# local (not referenced with another offset)
|
||||
self.rewriteLastShortToLong(offset)
|
||||
self.f.seek(-10, os.SEEK_CUR)
|
||||
self.writeShort(TiffTags.LONG) # rewrite the type to LONG
|
||||
self.f.seek(8, os.SEEK_CUR)
|
||||
self._rewriteLast(offset, field_size, new_field_size)
|
||||
# Move back past the new offset, past 'count', and before 'field_type'
|
||||
rewind = -new_field_size - 4 - 2
|
||||
self.f.seek(rewind, os.SEEK_CUR)
|
||||
self.writeShort(new_field_size) # rewrite the type
|
||||
self.f.seek(2 - rewind, os.SEEK_CUR)
|
||||
else:
|
||||
self._rewriteLast(offset, field_size)
|
||||
|
||||
|
|
|
@ -47,6 +47,8 @@ def deprecate(
|
|||
raise RuntimeError(msg)
|
||||
elif when == 12:
|
||||
removed = "Pillow 12 (2025-10-15)"
|
||||
elif when == 13:
|
||||
removed = "Pillow 13 (2026-10-15)"
|
||||
else:
|
||||
msg = f"Unknown removal version: {when}. Update {__name__}?"
|
||||
raise ValueError(msg)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Master version for Pillow
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "11.1.0.dev0"
|
||||
__version__ = "11.2.0.dev0"
|
||||
|
|
|
@ -127,6 +127,7 @@ features: dict[str, tuple[str, str | bool, str | None]] = {
|
|||
"fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"),
|
||||
"harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"),
|
||||
"libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"),
|
||||
"mozjpeg": ("PIL._imaging", "HAVE_MOZJPEG", "libjpeg_turbo_version"),
|
||||
"zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"),
|
||||
"libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"),
|
||||
"xcb": ("PIL._imaging", "HAVE_XCB", None),
|
||||
|
@ -300,7 +301,8 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
|
|||
if name == "jpg":
|
||||
libjpeg_turbo_version = version_feature("libjpeg_turbo")
|
||||
if libjpeg_turbo_version is not None:
|
||||
v = "libjpeg-turbo " + libjpeg_turbo_version
|
||||
v = "mozjpeg" if check_feature("mozjpeg") else "libjpeg-turbo"
|
||||
v += " " + libjpeg_turbo_version
|
||||
if v is None:
|
||||
v = version(name)
|
||||
if v is not None:
|
||||
|
|
|
@ -76,6 +76,13 @@
|
|||
|
||||
#ifdef HAVE_LIBJPEG
|
||||
#include "jconfig.h"
|
||||
#ifdef LIBJPEG_TURBO_VERSION
|
||||
#define JCONFIG_INCLUDED
|
||||
#ifdef __CYGWIN__
|
||||
#define _BASETSD_H
|
||||
#endif
|
||||
#include "jpeglib.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_LIBZ
|
||||
|
@ -4367,6 +4374,15 @@ setup_module(PyObject *m) {
|
|||
Py_INCREF(have_libjpegturbo);
|
||||
PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo);
|
||||
|
||||
PyObject *have_mozjpeg;
|
||||
#ifdef JPEG_C_PARAM_SUPPORTED
|
||||
have_mozjpeg = Py_True;
|
||||
#else
|
||||
have_mozjpeg = Py_False;
|
||||
#endif
|
||||
Py_INCREF(have_mozjpeg);
|
||||
PyModule_AddObject(m, "HAVE_MOZJPEG", have_mozjpeg);
|
||||
|
||||
PyObject *have_libimagequant;
|
||||
#ifdef HAVE_LIBIMAGEQUANT
|
||||
have_libimagequant = Py_True;
|
||||
|
|
|
@ -339,29 +339,23 @@ text_layout_raqm(
|
|||
len = PySequence_Fast_GET_SIZE(seq);
|
||||
for (j = 0; j < len; j++) {
|
||||
PyObject *item = PySequence_Fast_GET_ITEM(seq, j);
|
||||
char *feature = NULL;
|
||||
Py_ssize_t size = 0;
|
||||
PyObject *bytes;
|
||||
|
||||
if (!PyUnicode_Check(item)) {
|
||||
Py_DECREF(seq);
|
||||
PyErr_SetString(PyExc_TypeError, "expected a string");
|
||||
goto failed;
|
||||
}
|
||||
bytes = PyUnicode_AsUTF8String(item);
|
||||
if (bytes == NULL) {
|
||||
|
||||
Py_ssize_t size;
|
||||
const char *feature = PyUnicode_AsUTF8AndSize(item, &size);
|
||||
if (feature == NULL) {
|
||||
Py_DECREF(seq);
|
||||
goto failed;
|
||||
}
|
||||
feature = PyBytes_AS_STRING(bytes);
|
||||
size = PyBytes_GET_SIZE(bytes);
|
||||
if (!raqm_add_font_feature(rq, feature, size)) {
|
||||
Py_DECREF(seq);
|
||||
Py_DECREF(bytes);
|
||||
PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed");
|
||||
goto failed;
|
||||
}
|
||||
Py_DECREF(bytes);
|
||||
}
|
||||
Py_DECREF(seq);
|
||||
}
|
||||
|
|
|
@ -44,8 +44,6 @@
|
|||
defines their own types with the same names, so we need to be able to undef
|
||||
ours before including the JPEG code. */
|
||||
|
||||
#if __STDC_VERSION__ >= 199901L /* C99+ */
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define INT8 int8_t
|
||||
|
@ -55,34 +53,6 @@
|
|||
#define INT32 int32_t
|
||||
#define UINT32 uint32_t
|
||||
|
||||
#else /* < C99 */
|
||||
|
||||
#define INT8 signed char
|
||||
|
||||
#if SIZEOF_SHORT == 2
|
||||
#define INT16 short
|
||||
#elif SIZEOF_INT == 2
|
||||
#define INT16 int
|
||||
#else
|
||||
#error Cannot find required 16-bit integer type
|
||||
#endif
|
||||
|
||||
#if SIZEOF_SHORT == 4
|
||||
#define INT32 short
|
||||
#elif SIZEOF_INT == 4
|
||||
#define INT32 int
|
||||
#elif SIZEOF_LONG == 4
|
||||
#define INT32 long
|
||||
#else
|
||||
#error Cannot find required 32-bit integer type
|
||||
#endif
|
||||
|
||||
#define UINT8 unsigned char
|
||||
#define UINT16 unsigned INT16
|
||||
#define UINT32 unsigned INT32
|
||||
|
||||
#endif /* < C99 */
|
||||
|
||||
#endif /* not WIN */
|
||||
|
||||
/* assume IEEE; tweak if necessary (patches are welcome) */
|
||||
|
|
|
@ -134,7 +134,16 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
|
|||
return -1;
|
||||
}
|
||||
|
||||
/* Compressor configuration */
|
||||
/* Compressor configuration */
|
||||
#ifdef JPEG_C_PARAM_SUPPORTED
|
||||
/* MozJPEG */
|
||||
if (!context->progressive) {
|
||||
/* Do not use MozJPEG progressive default */
|
||||
jpeg_c_set_int_param(
|
||||
&context->cinfo, JINT_COMPRESS_PROFILE, JCP_FASTEST
|
||||
);
|
||||
}
|
||||
#endif
|
||||
jpeg_set_defaults(&context->cinfo);
|
||||
|
||||
/* Prevent RGB -> YCbCr conversion */
|
||||
|
|
|
@ -1664,6 +1664,7 @@ static struct {
|
|||
{"RGBA", "RGBaXX", 48, unpackRGBaskip2},
|
||||
{"RGBA", "RGBa;16L", 64, unpackRGBa16L},
|
||||
{"RGBA", "RGBa;16B", 64, unpackRGBa16B},
|
||||
{"RGBA", "BGR", 24, ImagingUnpackBGR},
|
||||
{"RGBA", "BGRa", 32, unpackBGRa},
|
||||
{"RGBA", "RGBA;I", 32, unpackRGBAI},
|
||||
{"RGBA", "RGBA;L", 32, unpackRGBAL},
|
||||
|
|
|
@ -11,10 +11,11 @@ For more extensive info, see the [Windows build instructions](build.rst).
|
|||
* Requires Microsoft Visual Studio 2017 or newer with C++ component.
|
||||
* Requires NASM for libjpeg-turbo, a required dependency when using this script.
|
||||
* Requires CMake 3.15 or newer (available as Visual Studio component).
|
||||
* Tested on Windows Server 2019 with Visual Studio 2019 Community and Visual Studio 2022 Community (AppVeyor).
|
||||
* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions).
|
||||
* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise and Windows Server
|
||||
2019 with Visual Studio 2019 Enterprise (GitHub Actions).
|
||||
|
||||
Here's an example script to build on Windows:
|
||||
|
||||
The following is a simplified version of the script used on AppVeyor:
|
||||
```
|
||||
set PYTHON=C:\Python39\bin
|
||||
cd /D C:\Pillow\winbuild
|
||||
|
|
|
@ -6,7 +6,7 @@ Building Pillow on Windows
|
|||
be sufficient.
|
||||
|
||||
This page describes the steps necessary to build Pillow using the same
|
||||
scripts used on GitHub Actions and AppVeyor CIs.
|
||||
scripts used on GitHub Actions CI.
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
@ -112,7 +112,7 @@ directory.
|
|||
Example
|
||||
-------
|
||||
|
||||
The following is a simplified version of the script used on AppVeyor::
|
||||
Here's an example script to build on Windows::
|
||||
|
||||
set PYTHON=C:\Python39\bin
|
||||
cd /D C:\Pillow\winbuild
|
||||
|
|
|
@ -116,14 +116,13 @@ V = {
|
|||
"HARFBUZZ": "10.1.0",
|
||||
"JPEGTURBO": "3.1.0",
|
||||
"LCMS2": "2.16",
|
||||
"LIBPNG": "1.6.44",
|
||||
"LIBPNG": "1.6.45",
|
||||
"LIBWEBP": "1.5.0",
|
||||
"OPENJPEG": "2.5.3",
|
||||
"TIFF": "4.6.0",
|
||||
"XZ": "5.6.3",
|
||||
"ZLIBNG": "2.2.2",
|
||||
"ZLIBNG": "2.2.3",
|
||||
}
|
||||
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
|
||||
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
|
||||
|
||||
|
||||
|
@ -241,8 +240,8 @@ DEPS: dict[str, dict[str, Any]] = {
|
|||
},
|
||||
"libpng": {
|
||||
"url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/"
|
||||
f"lpng{V['LIBPNG_DOTLESS']}.zip/download",
|
||||
"filename": f"lpng{V['LIBPNG_DOTLESS']}.zip",
|
||||
f"FILENAME/download",
|
||||
"filename": f"libpng-{V['LIBPNG']}.tar.gz",
|
||||
"license": "LICENSE",
|
||||
"build": [
|
||||
*cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"),
|
||||
|
|
Loading…
Reference in New Issue
Block a user