mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-01-27 09:44:31 +03:00
Merge branch 'main' into mode_enums
This commit is contained in:
commit
e76fcd5b2e
|
@ -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
|
||||
|
|
|
@ -21,7 +21,7 @@ set -e
|
|||
|
||||
if [[ $(uname) != CYGWIN* ]]; then
|
||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
||||
ghostscript libjpeg-turbo-progs libopenjp2-7-dev\
|
||||
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||
sway wl-clipboard libopenblas-dev
|
||||
fi
|
||||
|
|
|
@ -1 +1 @@
|
|||
cibuildwheel==2.21.3
|
||||
cibuildwheel==2.22.0
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mypy==1.13.0
|
||||
mypy==1.14.1
|
||||
IceSpringPySideStubs-PyQt6
|
||||
IceSpringPySideStubs-PySide6
|
||||
ipython
|
||||
|
|
5
.github/CONTRIBUTING.md
vendored
5
.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,9 +17,8 @@ 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.
|
||||
- Do not add to the [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged.
|
||||
|
||||
## 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
|
||||
|
|
11
.github/release-drafter.yml
vendored
11
.github/release-drafter.yml
vendored
|
@ -3,18 +3,19 @@ tag-template: "$NEXT_MINOR_VERSION"
|
|||
change-template: '- $TITLE #$NUMBER [@$AUTHOR]'
|
||||
|
||||
categories:
|
||||
- title: "Dependencies"
|
||||
label: "Dependency"
|
||||
- title: "Removals"
|
||||
label: "Removal"
|
||||
- title: "Deprecations"
|
||||
label: "Deprecation"
|
||||
- title: "Documentation"
|
||||
label: "Documentation"
|
||||
- title: "Removals"
|
||||
label: "Removal"
|
||||
- title: "Dependencies"
|
||||
label: "Dependency"
|
||||
- title: "Testing"
|
||||
label: "Testing"
|
||||
- title: "Type hints"
|
||||
label: "Type hints"
|
||||
- title: "Other changes"
|
||||
|
||||
exclude-labels:
|
||||
- "changelog: skip"
|
||||
|
@ -23,6 +24,4 @@ template: |
|
|||
|
||||
https://pillow.readthedocs.io/en/stable/releasenotes/$NEXT_MINOR_VERSION.html
|
||||
|
||||
## Changes
|
||||
|
||||
$CHANGES
|
||||
|
|
2
.github/workflows/macos-install.sh
vendored
2
.github/workflows/macos-install.sh
vendored
|
@ -8,8 +8,8 @@ fi
|
|||
brew install \
|
||||
freetype \
|
||||
ghostscript \
|
||||
jpeg-turbo \
|
||||
libimagequant \
|
||||
libjpeg \
|
||||
libtiff \
|
||||
little-cms2 \
|
||||
openjpeg \
|
||||
|
|
7
.github/workflows/test-cygwin.yml
vendored
7
.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++
|
||||
|
@ -133,11 +133,12 @@ jobs:
|
|||
- name: After success
|
||||
run: |
|
||||
bash.exe .ci/after_success.sh
|
||||
rm C:\cygwin\bin\bash.EXE
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
files: ./coverage.xml
|
||||
flags: GHA_Cygwin
|
||||
name: Cygwin Python 3.${{ matrix.python-minor-version }}
|
||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
||||
|
|
18
.github/workflows/test-docker.yml
vendored
18
.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,18 +91,18 @@ 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@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
flags: GHA_Docker
|
||||
name: ${{ matrix.docker }}
|
||||
|
|
18
.github/workflows/test-mingw.yml
vendored
18
.github/workflows/test-mingw.yml
vendored
|
@ -66,18 +66,18 @@ 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-setuptools \
|
||||
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 \
|
||||
mingw-w64-x86_64-python-pyqt6
|
||||
|
||||
python3 -m ensurepip
|
||||
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
|
||||
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
||||
- name: Build Pillow
|
||||
run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install .
|
||||
run: CFLAGS="-coverage" python3 -m pip install .
|
||||
|
||||
- name: Test Pillow
|
||||
run: |
|
||||
|
@ -85,9 +85,9 @@ jobs:
|
|||
.ci/test.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
files: ./coverage.xml
|
||||
flags: GHA_Windows
|
||||
name: "MSYS2 MinGW"
|
||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
||||
|
|
38
.github/workflows/test-windows.yml
vendored
38
.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,28 +72,21 @@ jobs:
|
|||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
architecture: ${{ matrix.architecture }}
|
||||
cache: pip
|
||||
cache-dependency-path: ".github/workflows/test-windows.yml"
|
||||
|
||||
- name: Print build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: >
|
||||
python3 -m pip install
|
||||
coverage>=7.4.2
|
||||
defusedxml
|
||||
olefile
|
||||
pyroma
|
||||
pytest
|
||||
pytest-cov
|
||||
pytest-timeout
|
||||
- name: Upgrade pip
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
|
||||
- name: Install CPython dependencies
|
||||
if: "!contains(matrix.python-version, 'pypy')"
|
||||
run: >
|
||||
python3 -m pip install
|
||||
PyQt6
|
||||
if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'"
|
||||
run: |
|
||||
python3 -m pip install PyQt6
|
||||
|
||||
- name: Install dependencies
|
||||
id: install
|
||||
|
@ -188,7 +186,7 @@ jobs:
|
|||
- name: Build Pillow
|
||||
run: |
|
||||
$FLAGS="-C raqm=vendor -C fribidi=vendor"
|
||||
cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ."
|
||||
cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS .[tests]"
|
||||
& $env:pythonLocation\python.exe selftest.py --installed
|
||||
shell: pwsh
|
||||
|
||||
|
@ -223,9 +221,9 @@ jobs:
|
|||
shell: pwsh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
files: ./coverage.xml
|
||||
flags: GHA_Windows
|
||||
name: ${{ runner.os }} Python ${{ matrix.python-version }}
|
||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
||||
|
|
20
.github/workflows/test.yml
vendored
20
.github/workflows/test.yml
vendored
|
@ -42,6 +42,8 @@ jobs:
|
|||
]
|
||||
python-version: [
|
||||
"pypy3.10",
|
||||
"3.14",
|
||||
"3.13t",
|
||||
"3.13",
|
||||
"3.12",
|
||||
"3.11",
|
||||
|
@ -52,14 +54,14 @@ jobs:
|
|||
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
|
||||
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
|
||||
# Free-threaded
|
||||
- { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true }
|
||||
- { python-version: "3.13t", disable-gil: true }
|
||||
# M1 only available for 3.10+
|
||||
- { os: "macos-13", python-version: "3.9" }
|
||||
exclude:
|
||||
- { os: "macos-latest", python-version: "3.9" }
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}
|
||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -67,8 +69,7 @@ jobs:
|
|||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
if: "${{ !matrix.disable-gil }}"
|
||||
uses: Quansight-Labs/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
|
@ -77,13 +78,6 @@ jobs:
|
|||
".ci/*.sh"
|
||||
"pyproject.toml"
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }} (free-threaded)
|
||||
uses: deadsnakes/action@v3.2.0
|
||||
if: "${{ matrix.disable-gil }}"
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
nogil: ${{ matrix.disable-gil }}
|
||||
|
||||
- name: Set PYTHON_GIL
|
||||
if: "${{ matrix.disable-gil }}"
|
||||
run: |
|
||||
|
@ -116,7 +110,7 @@ jobs:
|
|||
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
||||
|
||||
- name: Register gcc problem matcher
|
||||
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'"
|
||||
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'"
|
||||
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
|
||||
|
||||
- name: Build
|
||||
|
@ -156,7 +150,7 @@ jobs:
|
|||
.ci/after_success.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
|
||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
|
|
179
.github/workflows/wheels-dependencies.sh
vendored
179
.github/workflows/wheels-dependencies.sh
vendored
|
@ -1,11 +1,33 @@
|
|||
#!/bin/bash
|
||||
# Define custom utilities
|
||||
# Test for macOS with [ -n "$IS_MACOS" ]
|
||||
if [ -z "$IS_MACOS" ]; then
|
||||
export MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
|
||||
export MB_ML_VER=${AUDITWHEEL_POLICY:9}
|
||||
|
||||
# Setup that needs to be done before multibuild utils are invoked
|
||||
PROJECTDIR=$(pwd)
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
# Safety check - macOS builds require that CIBW_ARCHS is set, and that it
|
||||
# only contains a single value (even though cibuildwheel allows multiple
|
||||
# values in CIBW_ARCHS).
|
||||
if [[ -z "$CIBW_ARCHS" ]]; then
|
||||
echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$CIBW_ARCHS" == *" "* ]]; then
|
||||
echo "ERROR: Pillow macOS builds only support a single architecture in CIBW_ARCHS."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build macOS dependencies in `build/darwin`
|
||||
# Install them into `build/deps/darwin`
|
||||
WORKDIR=$(pwd)/build/darwin
|
||||
BUILD_PREFIX=$(pwd)/build/deps/darwin
|
||||
else
|
||||
# Build prefix will default to /usr/local
|
||||
WORKDIR=$(pwd)/build
|
||||
MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
|
||||
MB_ML_VER=${AUDITWHEEL_POLICY:9}
|
||||
fi
|
||||
export PLAT=$CIBW_ARCHS
|
||||
PLAT=$CIBW_ARCHS
|
||||
|
||||
# Define custom utilities
|
||||
source wheels/multibuild/common_utils.sh
|
||||
source wheels/multibuild/library_builders.sh
|
||||
if [ -z "$IS_MACOS" ]; then
|
||||
|
@ -15,79 +37,95 @@ fi
|
|||
ARCHIVE_SDIR=pillow-depends-main
|
||||
|
||||
# Package versions for fresh source builds
|
||||
FREETYPE_VERSION=2.13.2
|
||||
HARFBUZZ_VERSION=10.0.1
|
||||
LIBPNG_VERSION=1.6.44
|
||||
JPEGTURBO_VERSION=3.0.4
|
||||
OPENJPEG_VERSION=2.5.2
|
||||
FREETYPE_VERSION=2.13.3
|
||||
HARFBUZZ_VERSION=10.1.0
|
||||
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
|
||||
if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
|
||||
ZLIB_VERSION=1.3.1
|
||||
else
|
||||
ZLIB_VERSION=1.2.8
|
||||
fi
|
||||
LIBWEBP_VERSION=1.4.0
|
||||
ZLIB_NG_VERSION=2.2.3
|
||||
LIBWEBP_VERSION=1.5.0
|
||||
BZIP2_VERSION=1.0.8
|
||||
LIBXCB_VERSION=1.17.0
|
||||
BROTLI_VERSION=1.1.0
|
||||
|
||||
function build_pkg_config {
|
||||
if [ -e pkg-config-stamp ]; then return; fi
|
||||
# This essentially duplicates the Homebrew recipe
|
||||
ORIGINAL_CFLAGS=$CFLAGS
|
||||
CFLAGS="$CFLAGS -Wno-int-conversion"
|
||||
build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \
|
||||
--disable-debug --disable-host-tool --with-internal-glib \
|
||||
--with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \
|
||||
--with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include
|
||||
CFLAGS=$ORIGINAL_CFLAGS
|
||||
export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config
|
||||
touch pkg-config-stamp
|
||||
}
|
||||
|
||||
function build_zlib_ng {
|
||||
if [ -e zlib-stamp ]; then return; fi
|
||||
fetch_unpack https://github.com/zlib-ng/zlib-ng/archive/$ZLIB_NG_VERSION.tar.gz zlib-ng-$ZLIB_NG_VERSION.tar.gz
|
||||
(cd zlib-ng-$ZLIB_NG_VERSION \
|
||||
&& ./configure --prefix=$BUILD_PREFIX --zlib-compat \
|
||||
&& make -j4 \
|
||||
&& make install)
|
||||
touch zlib-stamp
|
||||
}
|
||||
|
||||
function build_brotli {
|
||||
local cmake=$(get_modern_cmake)
|
||||
if [ -e brotli-stamp ]; then return; fi
|
||||
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
|
||||
(cd $out_dir \
|
||||
&& $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
|
||||
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
|
||||
&& make install)
|
||||
if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
|
||||
cp /usr/local/lib64/libbrotli* /usr/local/lib
|
||||
cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig
|
||||
fi
|
||||
touch brotli-stamp
|
||||
}
|
||||
|
||||
function build_harfbuzz {
|
||||
if [ -e harfbuzz-stamp ]; then return; fi
|
||||
python3 -m pip install meson ninja
|
||||
|
||||
local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz)
|
||||
local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz)
|
||||
(cd $out_dir \
|
||||
&& meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled)
|
||||
&& meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled)
|
||||
(cd $out_dir/build \
|
||||
&& meson install)
|
||||
if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
|
||||
cp /usr/local/lib64/libharfbuzz* /usr/local/lib
|
||||
fi
|
||||
touch harfbuzz-stamp
|
||||
}
|
||||
|
||||
function build {
|
||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
sudo chown -R runner /usr/local
|
||||
fi
|
||||
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_new_zlib
|
||||
build_zlib_ng
|
||||
|
||||
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||
if [ -n "$IS_MACOS" ]; then
|
||||
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
|
||||
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
|
||||
build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib
|
||||
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
||||
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig
|
||||
fi
|
||||
else
|
||||
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
|
||||
sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc
|
||||
fi
|
||||
build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib
|
||||
|
||||
build_libjpeg_turbo
|
||||
build_tiff
|
||||
if [ -n "$IS_MACOS" ]; then
|
||||
# Custom tiff build to include jpeg; by default, configure won't include
|
||||
# headers/libs in the custom macOS prefix. Explicitly disable webp,
|
||||
# libdeflate and zstd, because on x86_64 macs, it will pick up the
|
||||
# Homebrew versions of those libraries from /usr/local.
|
||||
build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \
|
||||
--with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \
|
||||
--disable-webp --disable-libdeflate --disable-zstd
|
||||
else
|
||||
build_tiff
|
||||
fi
|
||||
|
||||
build_libpng
|
||||
build_lcms2
|
||||
build_openjpeg
|
||||
|
@ -97,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
|
||||
|
@ -112,32 +152,47 @@ function build {
|
|||
build_harfbuzz
|
||||
}
|
||||
|
||||
# Perform all dependency builds in the build subfolder.
|
||||
mkdir -p $WORKDIR
|
||||
pushd $WORKDIR > /dev/null
|
||||
|
||||
# Any stuff that you need to do before you start building the wheels
|
||||
# Runs in the root directory of this repository.
|
||||
curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
|
||||
untar pillow-depends-main.zip
|
||||
if [[ ! -d $WORKDIR/pillow-depends-main ]]; then
|
||||
if [[ ! -f $PROJECTDIR/pillow-depends-main.zip ]]; then
|
||||
echo "Download pillow dependency sources..."
|
||||
curl -fSL -o $PROJECTDIR/pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
|
||||
fi
|
||||
echo "Unpacking pillow dependency sources..."
|
||||
untar $PROJECTDIR/pillow-depends-main.zip
|
||||
fi
|
||||
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
# libdeflate may cause a minimum target error when repairing the wheel
|
||||
# libtiff and libxcb cause a conflict with building libtiff and libxcb
|
||||
# libxau and libxdmcp cause an issue on macOS < 11
|
||||
# remove cairo to fix building harfbuzz on arm64
|
||||
# remove lcms2 and libpng to fix building openjpeg on arm64
|
||||
# remove jpeg-turbo to avoid inclusion on arm64
|
||||
# remove webp and zstd to avoid inclusion on x86_64
|
||||
# curl from brew requires zstd, use system curl
|
||||
brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd
|
||||
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
brew remove --ignore-dependencies jpeg-turbo
|
||||
else
|
||||
brew remove --ignore-dependencies libdeflate webp
|
||||
fi
|
||||
# Homebrew (or similar packaging environments) install can contain some of
|
||||
# the libraries that we're going to build. However, they may be compiled
|
||||
# with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use,
|
||||
# and they may bring in other dependencies that we don't want. The same will
|
||||
# be true of any other locations on the path. To avoid conflicts, strip the
|
||||
# path down to the bare minimum (which, on macOS, won't include any
|
||||
# development dependencies).
|
||||
export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
|
||||
export CMAKE_PREFIX_PATH=$BUILD_PREFIX
|
||||
|
||||
brew install pkg-config
|
||||
# Ensure the basic structure of the build prefix directory exists.
|
||||
mkdir -p "$BUILD_PREFIX/bin"
|
||||
mkdir -p "$BUILD_PREFIX/lib"
|
||||
|
||||
# Ensure pkg-config is available
|
||||
build_pkg_config
|
||||
# Ensure cmake is available
|
||||
python3 -m pip install cmake
|
||||
fi
|
||||
|
||||
wrap_wheel_builder build
|
||||
|
||||
# Return to the project root to finish the build
|
||||
popd > /dev/null
|
||||
|
||||
# Append licenses
|
||||
for filename in wheels/dependency_licenses/*; do
|
||||
echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE
|
||||
|
|
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 }
|
||||
|
|
20
.github/workflows/wheels-test.sh
vendored
20
.github/workflows/wheels-test.sh
vendored
|
@ -1,12 +1,24 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Ensure fribidi is installed by the system.
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
brew install fribidi
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||
if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then
|
||||
sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib
|
||||
# If Homebrew is on the path during the build, it may leak into the wheels.
|
||||
# However, we *do* need Homebrew to provide a copy of fribidi for
|
||||
# testing purposes so that we can verify the fribidi shim works as expected.
|
||||
if [[ "$(uname -m)" == "x86_64" ]]; then
|
||||
HOMEBREW_PREFIX=/usr/local
|
||||
else
|
||||
HOMEBREW_PREFIX=/opt/homebrew
|
||||
fi
|
||||
$HOMEBREW_PREFIX/bin/brew install fribidi
|
||||
|
||||
# Add the lib folder for fribidi so that the vendored library can be found.
|
||||
# Don't use $HOMEWBREW_PREFIX/lib directly - use the lib folder where the
|
||||
# installed copy of fribidi is cellared. This ensures we don't pick up the
|
||||
# Homebrew version of any other library that we're dependent on (most notably,
|
||||
# freetype).
|
||||
export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_PREFIX/lib/libfribidi.dylib))
|
||||
elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
|
||||
apk add curl fribidi
|
||||
else
|
||||
|
|
81
.github/workflows/wheels.yml
vendored
81
.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_PRERELEASE_PYTHONS: True
|
||||
# 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,10 +105,11 @@ jobs:
|
|||
env:
|
||||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||
CIBW_BUILD: ${{ matrix.build }}
|
||||
CIBW_FREE_THREADED_SUPPORT: True
|
||||
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_PRERELEASE_PYTHONS: True
|
||||
CIBW_SKIP: pp39-*
|
||||
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
||||
|
||||
|
@ -228,8 +184,7 @@ jobs:
|
|||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
||||
CIBW_CACHE_PATH: "C:\\cibw"
|
||||
CIBW_FREE_THREADED_SUPPORT: True
|
||||
CIBW_PRERELEASE_PYTHONS: True
|
||||
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
|
||||
CIBW_SKIP: pp39-*
|
||||
CIBW_TEST_SKIP: "*-win_arm64"
|
||||
CIBW_TEST_COMMAND: 'docker run --rm
|
||||
|
@ -265,8 +220,6 @@ jobs:
|
|||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
cache: pip
|
||||
cache-dependency-path: "Makefile"
|
||||
|
||||
- run: make sdist
|
||||
|
||||
|
@ -277,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:
|
||||
|
@ -294,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:
|
||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -19,6 +19,7 @@ lib64/
|
|||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheelhouse/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
@ -90,5 +91,9 @@ Tests/images/msp
|
|||
Tests/images/picins
|
||||
Tests/images/sunraster
|
||||
|
||||
# Test and dependency downloads
|
||||
pillow-depends-main.zip
|
||||
pillow-test-images.zip
|
||||
|
||||
# pyinstaller
|
||||
*.spec
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.7.2
|
||||
rev: v0.8.6
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--exit-non-zero-on-fix]
|
||||
|
@ -11,7 +11,7 @@ repos:
|
|||
- id: black
|
||||
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.10
|
||||
rev: 1.8.0
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: [--severity-level=high]
|
||||
|
@ -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.3
|
||||
rev: v19.1.6
|
||||
hooks:
|
||||
- id: clang-format
|
||||
types: [c]
|
||||
|
@ -50,12 +50,17 @@ repos:
|
|||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.29.4
|
||||
rev: 0.30.0
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
- id: check-readthedocs
|
||||
- id: check-renovate
|
||||
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
|
@ -67,7 +72,7 @@ repos:
|
|||
- id: pyproject-fmt
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.22
|
||||
rev: v0.23
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
additional_dependencies: [trove-classifiers>=2024.10.12]
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
version: 2
|
||||
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
formats: [pdf]
|
||||
|
||||
build:
|
||||
|
|
16
CHANGES.rst
16
CHANGES.rst
|
@ -2,20 +2,12 @@
|
|||
Changelog (Pillow)
|
||||
==================
|
||||
|
||||
11.1.0 (unreleased)
|
||||
-------------------
|
||||
11.1.0 and newer
|
||||
----------------
|
||||
|
||||
- Detach PyQt6 QPixmap instance before returning #8509
|
||||
[radarhere]
|
||||
See GitHub Releases:
|
||||
|
||||
- Corrected EMF DPI #8485
|
||||
[radarhere]
|
||||
|
||||
- Fix IFDRational with a zero denominator #8474
|
||||
[radarhere]
|
||||
|
||||
- Fixed disabling a feature during install #8469
|
||||
[radarhere]
|
||||
- https://github.com/python-pillow/Pillow/releases
|
||||
|
||||
11.0.0 (2024-10-15)
|
||||
-------------------
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
|
|||
|
||||
Pillow is the friendly PIL fork. It is
|
||||
|
||||
Copyright © 2010-2024 by Jeffrey A. Clark and contributors
|
||||
Copyright © 2010 by Jeffrey A. Clark and contributors
|
||||
|
||||
Like PIL, Pillow is licensed under the open source MIT-CMU License:
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
@ -107,7 +104,7 @@ The core image library is designed for fast access to data stored in a few basic
|
|||
- [Issues](https://github.com/python-pillow/Pillow/issues)
|
||||
- [Pull requests](https://github.com/python-pillow/Pillow/pulls)
|
||||
- [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html)
|
||||
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
|
||||
- [Changelog](https://github.com/python-pillow/Pillow/releases)
|
||||
- [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork)
|
||||
|
||||
## Report a Vulnerability
|
||||
|
|
|
@ -9,10 +9,9 @@ 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`
|
||||
* [ ] Update `CHANGES.rst`.
|
||||
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
|
||||
* [ ] Create branch and tag for release e.g.:
|
||||
```bash
|
||||
|
@ -34,13 +33,12 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
|||
Released as needed for security, installation or critical bug fixes.
|
||||
|
||||
* [ ] Make necessary changes in `main` branch.
|
||||
* [ ] Update `CHANGES.rst`.
|
||||
* [ ] Check out release branch e.g.:
|
||||
```bash
|
||||
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.:
|
||||
|
|
|
@ -34,6 +34,7 @@ def test_wheel_features() -> None:
|
|||
"fribidi",
|
||||
"harfbuzz",
|
||||
"libjpeg_turbo",
|
||||
"zlib_ng",
|
||||
"xcb",
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 486 B After Width: | Height: | Size: 533 B |
BIN
Tests/images/jfif_unit_cm.jpg
Normal file
BIN
Tests/images/jfif_unit_cm.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 391 B |
|
@ -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()
|
||||
|
|
|
@ -388,10 +388,12 @@ class TestColorLut3DFilter:
|
|||
|
||||
table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16)
|
||||
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
|
||||
assert isinstance(lut.table, numpy.ndarray)
|
||||
assert lut.table.shape == (table.size,)
|
||||
|
||||
table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16)
|
||||
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
|
||||
assert isinstance(lut.table, numpy.ndarray)
|
||||
assert lut.table.shape == (table.size,)
|
||||
|
||||
# Check application
|
||||
|
|
|
@ -36,10 +36,11 @@ def test_version() -> None:
|
|||
else:
|
||||
assert function(name) == version
|
||||
if name != "PIL":
|
||||
if name == "zlib" and version is not None:
|
||||
version = re.sub(".zlib-ng$", "", version)
|
||||
elif name == "libtiff" and version is not None:
|
||||
version = re.sub("t$", "", version)
|
||||
if version is not None:
|
||||
if name == "zlib" and features.check_feature("zlib_ng"):
|
||||
version = re.sub(".zlib-ng$", "", version)
|
||||
elif name == "libtiff":
|
||||
version = re.sub("t$", "", version)
|
||||
assert version is None or re.search(r"\d+(\.\d+)*$", version)
|
||||
|
||||
for module in features.modules:
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None:
|
|||
im.save(temp_file)
|
||||
assert handler.saved
|
||||
|
||||
BufrStubImagePlugin._handler = None
|
||||
BufrStubImagePlugin.register_handler(None)
|
||||
|
|
|
@ -4,8 +4,6 @@ import pytest
|
|||
|
||||
from PIL import ContainerIO, Image
|
||||
|
||||
from .helper import hopper
|
||||
|
||||
TEST_FILE = "Tests/images/dummy.container"
|
||||
|
||||
|
||||
|
@ -15,15 +13,15 @@ def test_sanity() -> None:
|
|||
|
||||
|
||||
def test_isatty() -> None:
|
||||
with hopper() as im:
|
||||
container = ContainerIO.ContainerIO(im, 0, 0)
|
||||
with open(TEST_FILE, "rb") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 0, 0)
|
||||
|
||||
assert container.isatty() is False
|
||||
|
||||
|
||||
def test_seekable() -> None:
|
||||
with hopper() as im:
|
||||
container = ContainerIO.ContainerIO(im, 0, 0)
|
||||
with open(TEST_FILE, "rb") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 0, 0)
|
||||
|
||||
assert container.seekable() is True
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import warnings
|
|||
from collections.abc import Generator
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -1435,7 +1436,8 @@ def test_saving_rgba(tmp_path: Path) -> None:
|
|||
assert reloaded_rgba.load()[0, 0][3] == 0
|
||||
|
||||
|
||||
def test_optimizing_p_rgba(tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False}))
|
||||
def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
im1 = Image.new("P", (100, 100))
|
||||
|
@ -1447,7 +1449,7 @@ def test_optimizing_p_rgba(tmp_path: Path) -> None:
|
|||
im2 = Image.new("P", (100, 100))
|
||||
im2.putpalette(data, "RGBA")
|
||||
|
||||
im1.save(out, save_all=True, append_images=[im2])
|
||||
im1.save(out, save_all=True, append_images=[im2], **params)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert reloaded.n_frames == 2
|
||||
|
|
|
@ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None:
|
|||
im.save(temp_file)
|
||||
assert handler.saved
|
||||
|
||||
GribStubImagePlugin._handler = None
|
||||
GribStubImagePlugin.register_handler(None)
|
||||
|
|
|
@ -85,4 +85,4 @@ def test_handler(tmp_path: Path) -> None:
|
|||
im.save(temp_file)
|
||||
assert handler.saved
|
||||
|
||||
Hdf5StubImagePlugin._handler = None
|
||||
Hdf5StubImagePlugin.register_handler(None)
|
||||
|
|
|
@ -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,6 +181,10 @@ class TestFileJpeg:
|
|||
assert test(100, 200) == (100, 200)
|
||||
assert test(0) is None # square pixels
|
||||
|
||||
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)
|
||||
|
||||
@mark_if_feature_version(
|
||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||
)
|
||||
|
@ -277,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:
|
||||
|
@ -349,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) == {}
|
||||
|
||||
|
@ -420,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")
|
||||
|
@ -1000,8 +1010,13 @@ class TestFileJpeg:
|
|||
with Image.open(f) as reloaded:
|
||||
assert reloaded.info["xmp"] == b"XMP test"
|
||||
|
||||
im.info["xmp"] = b"1" * 65504
|
||||
im.save(f)
|
||||
# Check that XMP is not saved from image info
|
||||
reloaded.save(f)
|
||||
|
||||
with Image.open(f) as reloaded:
|
||||
assert "xmp" not in reloaded.info
|
||||
|
||||
im.save(f, xmp=b"1" * 65504)
|
||||
with Image.open(f) as reloaded:
|
||||
assert reloaded.info["xmp"] == b"1" * 65504
|
||||
|
||||
|
@ -1022,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()
|
||||
|
|
|
@ -325,6 +325,18 @@ def test_cmyk() -> None:
|
|||
assert im.getpixel((0, 0)) == (185, 134, 0, 0)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
||||
)
|
||||
@skip_unless_feature_version("jpg_2000", "2.5.3")
|
||||
def test_cmyk_save() -> None:
|
||||
with Image.open(f"{EXTRA_DIR}/issue205.jp2") as jp2:
|
||||
assert jp2.mode == "CMYK"
|
||||
|
||||
im = roundtrip(jp2)
|
||||
assert_image_equal(im, jp2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
||||
def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
|
||||
with Image.open("Tests/images/16bit.cropped" + ext) as im:
|
||||
|
@ -424,8 +436,9 @@ def test_pclr() -> None:
|
|||
|
||||
|
||||
def test_comment() -> None:
|
||||
with Image.open("Tests/images/comment.jp2") as im:
|
||||
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
|
||||
for path in ("Tests/images/9bit.j2k", "Tests/images/comment.jp2"):
|
||||
with Image.open(path) as im:
|
||||
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
|
||||
|
||||
# Test an image that is truncated partway through a codestream
|
||||
with open("Tests/images/comment.jp2", "rb") as fp:
|
||||
|
@ -479,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")
|
||||
|
@ -1098,6 +1094,25 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
assert_image_similar(base_im, im, 0.7)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
[
|
||||
"Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif",
|
||||
"Tests/images/old-style-jpeg-compression.tif",
|
||||
],
|
||||
)
|
||||
def test_buffering(self, test_file: str) -> None:
|
||||
# load exif first
|
||||
with Image.open(open(test_file, "rb", buffering=1048576)) as im:
|
||||
exif = dict(im.getexif())
|
||||
|
||||
# load image before exif
|
||||
with Image.open(open(test_file, "rb", buffering=1048576)) as im2:
|
||||
im2.load()
|
||||
exif_after_load = dict(im2.getexif())
|
||||
|
||||
assert exif == exif_after_load
|
||||
|
||||
@pytest.mark.valgrind_known_error(reason="Backtrace in Python Core")
|
||||
def test_sampleformat_not_corrupted(self) -> None:
|
||||
# Assert that a TIFF image with SampleFormat=UINT tag is not corrupted
|
||||
|
@ -1127,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:
|
||||
|
|
|
@ -297,3 +297,15 @@ def test_save_all() -> None:
|
|||
# Test that a single frame image will not be saved as an MPO
|
||||
jpg = roundtrip(im, save_all=True)
|
||||
assert "mp" not in jpg.info
|
||||
|
||||
|
||||
def test_save_xmp() -> None:
|
||||
im = Image.new("RGB", (1, 1))
|
||||
im2 = Image.new("RGB", (1, 1), "#f00")
|
||||
im2.encoderinfo = {"xmp": b"Second frame"}
|
||||
im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2])
|
||||
|
||||
assert im_reloaded.info["xmp"] == b"First frame"
|
||||
|
||||
im_reloaded.seek(1)
|
||||
assert im_reloaded.info["xmp"] == b"Second frame"
|
||||
|
|
|
@ -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
|
||||
|
@ -772,22 +772,18 @@ class TestFilePng:
|
|||
im.seek(1)
|
||||
|
||||
@pytest.mark.parametrize("buffer", (True, False))
|
||||
def test_save_stdout(self, buffer: bool) -> None:
|
||||
old_stdout = sys.stdout
|
||||
def test_save_stdout(self, buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||
|
||||
sys.stdout = mystdout
|
||||
monkeypatch.setattr(sys, "stdout", mystdout)
|
||||
|
||||
with Image.open(TEST_PNG_FILE) as im:
|
||||
im.save(sys.stdout, "PNG")
|
||||
|
||||
# Reset stdout
|
||||
sys.stdout = old_stdout
|
||||
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
with Image.open(mystdout) as reloaded:
|
||||
|
|
|
@ -367,22 +367,18 @@ def test_mimetypes(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("buffer", (True, False))
|
||||
def test_save_stdout(buffer: bool) -> None:
|
||||
old_stdout = sys.stdout
|
||||
def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||
|
||||
sys.stdout = mystdout
|
||||
monkeypatch.setattr(sys, "stdout", mystdout)
|
||||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.save(sys.stdout, "PPM")
|
||||
|
||||
# Reset stdout
|
||||
sys.stdout = old_stdout
|
||||
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
with Image.open(mystdout) as reloaded:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -35,6 +35,13 @@ def test_load() -> None:
|
|||
assert im.load()[0, 0] == (255, 255, 255)
|
||||
|
||||
|
||||
def test_load_zero_inch() -> None:
|
||||
b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x00" * 10)
|
||||
with pytest.raises(ValueError):
|
||||
with Image.open(b):
|
||||
pass
|
||||
|
||||
|
||||
def test_register_handler(tmp_path: Path) -> None:
|
||||
class TestHandler(ImageFile.StubHandler):
|
||||
methodCalled = False
|
||||
|
|
|
@ -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
|
||||
|
@ -793,6 +791,10 @@ class TestImage:
|
|||
ifd[36864] = b"0220"
|
||||
assert exif.get_ifd(0x8769) == {36864: b"0220"}
|
||||
|
||||
reloaded_exif = Image.Exif()
|
||||
reloaded_exif.load(exif.tobytes())
|
||||
assert reloaded_exif.get_ifd(0x8769) == {36864: b"0220"}
|
||||
|
||||
@mark_if_feature_version(
|
||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||
)
|
||||
|
@ -987,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:
|
||||
|
|
|
@ -104,20 +104,20 @@ def test_transposed() -> None:
|
|||
assert im.size == (590, 88)
|
||||
|
||||
|
||||
def test_load_first_unless_jpeg() -> None:
|
||||
def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Test that thumbnail() still uses draft() for JPEG
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
draft = im.draft
|
||||
original_draft = im.draft
|
||||
|
||||
def im_draft(
|
||||
mode: str, size: tuple[int, int]
|
||||
mode: str | None, size: tuple[int, int] | None
|
||||
) -> tuple[str, tuple[int, int, float, float]] | None:
|
||||
result = draft(mode, size)
|
||||
result = original_draft(mode, size)
|
||||
assert result is not None
|
||||
|
||||
return result
|
||||
|
||||
im.draft = im_draft
|
||||
monkeypatch.setattr(im, "draft", im_draft)
|
||||
|
||||
im.thumbnail((64, 64))
|
||||
|
||||
|
|
|
@ -1674,6 +1674,9 @@ def test_continuous_horizontal_edges_polygon() -> None:
|
|||
def test_discontiguous_corners_polygon() -> None:
|
||||
img, draw = create_base_image_draw((84, 68))
|
||||
draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK)
|
||||
draw.polygon(
|
||||
((82, 29), (82, 26), (82, 24), (67, 22), (52, 29), (52, 15), (67, 22)), BLACK
|
||||
)
|
||||
draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK)
|
||||
draw.polygon(
|
||||
((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)),
|
||||
|
|
|
@ -93,6 +93,19 @@ class TestImageFile:
|
|||
assert p.image is not None
|
||||
assert (48, 48) == p.image.size
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:Corrupt EXIF data")
|
||||
def test_incremental_tiff(self) -> None:
|
||||
with ImageFile.Parser() as p:
|
||||
with open("Tests/images/hopper.tif", "rb") as f:
|
||||
p.feed(f.read(1024))
|
||||
|
||||
# Check that insufficient data was given in the first feed
|
||||
assert not p.image
|
||||
|
||||
p.feed(f.read())
|
||||
assert p.image is not None
|
||||
assert (128, 128) == p.image.size
|
||||
|
||||
@skip_unless_feature("webp")
|
||||
def test_incremental_webp(self) -> None:
|
||||
with ImageFile.Parser() as p:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -74,6 +74,17 @@ def test_pickle_image(
|
|||
helper_pickle_file(tmp_path, protocol, test_file, test_mode)
|
||||
|
||||
|
||||
def test_pickle_jpeg() -> None:
|
||||
# Arrange
|
||||
with Image.open("Tests/images/hopper.jpg") as image:
|
||||
# Act: roundtrip
|
||||
unpickled_image = pickle.loads(pickle.dumps(image))
|
||||
|
||||
# Assert
|
||||
assert len(unpickled_image.layer) == 3
|
||||
assert unpickled_image.layers == 3
|
||||
|
||||
|
||||
def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
|
||||
# Arrange
|
||||
filename = str(tmp_path / "temp.pkl")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Documentation: https://docs.codecov.com/docs/codecov-yaml
|
||||
|
||||
codecov:
|
||||
# Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]"
|
||||
# Avoid "Missing base report" due to committing with "[CI skip]"
|
||||
# https://github.com/codecov/support/issues/363
|
||||
# https://docs.codecov.com/docs/comparing-commits
|
||||
allow_coverage_offsets: true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# install openjpeg
|
||||
|
||||
archive=openjpeg-2.5.2
|
||||
archive=openjpeg-2.5.3
|
||||
|
||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# install webp
|
||||
|
||||
archive=libwebp-1.4.0
|
||||
archive=libwebp-1.5.0
|
||||
|
||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
|
|||
|
||||
Pillow is the friendly PIL fork. It is
|
||||
|
||||
Copyright © 2010-2024 by Jeffrey A. Clark and contributors
|
||||
Copyright © 2010 by Jeffrey A. Clark and contributors
|
||||
|
||||
Like PIL, Pillow is licensed under the open source PIL
|
||||
Software License:
|
||||
|
|
|
@ -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/
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ master_doc = "index"
|
|||
project = "Pillow (PIL Fork)"
|
||||
copyright = (
|
||||
"1995-2011 Fredrik Lundh and contributors, "
|
||||
"2010-2024 Jeffrey A. Clark and contributors."
|
||||
"2010 Jeffrey A. Clark and contributors."
|
||||
)
|
||||
author = "Fredrik Lundh (PIL), Jeffrey A. Clark (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
|
||||
----------------
|
||||
|
||||
|
|
|
@ -572,10 +572,19 @@ JPEG 2000
|
|||
Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``,
|
||||
``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to
|
||||
``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel.
|
||||
Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``,
|
||||
``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports
|
||||
JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files
|
||||
(``.jp2`` or ``.jpx`` files).
|
||||
|
||||
.. versionadded:: 8.3.0
|
||||
Pillow can read (but not write) ``RGB``, ``RGBA``, and ``YCbCr`` images with
|
||||
subsampled components.
|
||||
|
||||
.. versionadded:: 10.4.0
|
||||
Pillow can read ``CMYK`` images with OpenJPEG 2.5.1 and later.
|
||||
|
||||
.. versionadded:: 11.1.0
|
||||
Pillow can write ``CMYK`` images with OpenJPEG 2.5.3 and later.
|
||||
|
||||
Pillow supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed
|
||||
JPEG 2000 files (``.jp2`` or ``.jpx`` files).
|
||||
|
||||
When loading, if you set the ``mode`` on the image prior to the
|
||||
:py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to
|
||||
|
@ -1199,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
|
||||
|
|
|
@ -678,7 +678,7 @@ Reading from URL
|
|||
|
||||
from PIL import Image
|
||||
from urllib.request import urlopen
|
||||
url = "https://python-pillow.org/assets/images/pillow-logo.png"
|
||||
url = "https://python-pillow.github.io/assets/images/pillow-logo.png"
|
||||
img = Image.open(urlopen(url))
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -58,7 +58,7 @@ Many of Pillow's features require external libraries:
|
|||
* **openjpeg** provides JPEG 2000 functionality.
|
||||
|
||||
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
|
||||
**2.4.0**, **2.5.0** and **2.5.2**.
|
||||
**2.4.0**, **2.5.0**, **2.5.2** and **2.5.3**.
|
||||
* Pillow does **not** support the earlier **1.5** series which ships
|
||||
with Debian Jessie.
|
||||
|
||||
|
@ -148,13 +148,7 @@ Many of Pillow's features require external libraries:
|
|||
The easiest way to install external libraries is via `Homebrew
|
||||
<https://brew.sh/>`_. After you install Homebrew, run::
|
||||
|
||||
brew install libjpeg libtiff little-cms2 openjpeg webp
|
||||
|
||||
To install libraqm on macOS use Homebrew to install its dependencies::
|
||||
|
||||
brew install freetype harfbuzz fribidi
|
||||
|
||||
Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
|
||||
brew install libjpeg libraqm libtiff little-cms2 openjpeg webp
|
||||
|
||||
.. tab:: Windows
|
||||
|
||||
|
@ -195,11 +189,6 @@ Many of Pillow's features require external libraries:
|
|||
mingw-w64-x86_64-libimagequant \
|
||||
mingw-w64-x86_64-libraqm
|
||||
|
||||
https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with
|
||||
MSYS2. To workaround this, before installing Pillow you must run::
|
||||
|
||||
export SETUPTOOLS_USE_DISTUTILS=stdlib
|
||||
|
||||
.. tab:: FreeBSD
|
||||
|
||||
.. Note:: Only FreeBSD 10 and 11 tested
|
||||
|
|
|
@ -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,20 +44,16 @@ 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 | |
|
||||
| Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 |
|
||||
| | PyPy3 | |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.13 | x86 |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.9 (MinGW) | x86-64 |
|
||||
| | 3.12 (MinGW) | x86-64 |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.9 (Cygwin) | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
|
@ -75,7 +73,9 @@ 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.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm |
|
||||
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0 |arm |
|
||||
| +----------------------------+------------------+ |
|
||||
| | 3.8 | 10.4.0 | |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
|
|
|
@ -54,6 +54,8 @@ 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.
|
||||
* ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library.
|
||||
|
|
80
docs/releasenotes/11.1.0.rst
Normal file
80
docs/releasenotes/11.1.0.rst
Normal file
|
@ -0,0 +1,80 @@
|
|||
11.1.0
|
||||
------
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
||||
ExifTags.IFD.Makernote
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
|
||||
``ExifTags.IFD.MakerNote``.
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
||||
Writing XMP bytes to JPEG and MPO
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Pillow 11.0.0 added writing XMP data to JPEG and MPO images::
|
||||
|
||||
im.info["xmp"] = b"test"
|
||||
im.save("out.jpg")
|
||||
|
||||
However, this meant that XMP data was automatically kept from an opened image,
|
||||
which is inconsistent with the rest of Pillow's behaviour. This functionality
|
||||
has been removed. To write XMP data, the ``xmp`` argument can still be used for
|
||||
JPEG files::
|
||||
|
||||
im.save("out.jpg", xmp=b"test")
|
||||
|
||||
To save XMP data to the second frame of an MPO image, ``encoderinfo`` can now
|
||||
be used::
|
||||
|
||||
second_im.encoderinfo = {"xmp": b"test"}
|
||||
im.save("out.mpo", save_all=True, append_images=[second_im])
|
||||
|
||||
API Additions
|
||||
=============
|
||||
|
||||
Check for zlib-ng
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can check if Pillow has been built against the zlib-ng version of the
|
||||
zlib library, and what version of zlib-ng is being used::
|
||||
|
||||
from PIL import features
|
||||
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
|
||||
=============
|
||||
|
||||
Reading JPEG 2000 comments
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
When opening a JPEG 2000 image, the comment may now be read into
|
||||
:py:attr:`~PIL.Image.Image.info` for J2K images, not just JP2 images.
|
||||
|
||||
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
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Wheels are now built against zlib-ng for improved speed. In tests, saving a PNG
|
||||
was found to be more than twice as fast at higher compression levels.
|
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,8 @@ expected to be backported to earlier versions.
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
11.2.0
|
||||
11.1.0
|
||||
11.0.0
|
||||
10.4.0
|
||||
10.3.0
|
||||
|
|
|
@ -56,7 +56,7 @@ optional-dependencies.mic = [
|
|||
]
|
||||
optional-dependencies.tests = [
|
||||
"check-manifest",
|
||||
"coverage",
|
||||
"coverage>=7.4.2",
|
||||
"defusedxml",
|
||||
"markdown2",
|
||||
"olefile",
|
||||
|
@ -65,6 +65,7 @@ optional-dependencies.tests = [
|
|||
"pytest",
|
||||
"pytest-cov",
|
||||
"pytest-timeout",
|
||||
"trove-classifiers>=2024.10.12",
|
||||
]
|
||||
optional-dependencies.typing = [
|
||||
"typing-extensions; python_version<'3.10'",
|
||||
|
@ -72,10 +73,10 @@ optional-dependencies.typing = [
|
|||
optional-dependencies.xmp = [
|
||||
"defusedxml",
|
||||
]
|
||||
urls.Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst"
|
||||
urls.Changelog = "https://github.com/python-pillow/Pillow/releases"
|
||||
urls.Documentation = "https://pillow.readthedocs.io"
|
||||
urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi"
|
||||
urls.Homepage = "https://python-pillow.org"
|
||||
urls.Homepage = "https://python-pillow.github.io"
|
||||
urls.Mastodon = "https://fosstodon.org/@pillow"
|
||||
urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
|
||||
urls.Source = "https://github.com/python-pillow/Pillow"
|
||||
|
@ -93,10 +94,18 @@ 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"
|
||||
# Disable platform guessing on macOS
|
||||
macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable"
|
||||
|
||||
test-command = "cd {project} && .github/workflows/wheels-test.sh"
|
||||
test-extras = "tests"
|
||||
|
||||
[tool.cibuildwheel.macos.environment]
|
||||
PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
|
||||
DYLD_LIBRARY_PATH = "$(pwd)/build/deps/darwin/lib"
|
||||
|
||||
[tool.black]
|
||||
exclude = "wheels/multibuild"
|
||||
|
||||
|
|
24
setup.py
24
setup.py
|
@ -347,7 +347,7 @@ class pil_build_ext(build_ext):
|
|||
for x in ("raqm", "fribidi")
|
||||
]
|
||||
+ [
|
||||
("disable-platform-guessing", None, "Disable platform guessing on Linux"),
|
||||
("disable-platform-guessing", None, "Disable platform guessing"),
|
||||
("debug", None, "Debug logging"),
|
||||
]
|
||||
+ [("add-imaging-libs=", None, "Add libs to _imaging build")]
|
||||
|
@ -396,13 +396,14 @@ class pil_build_ext(build_ext):
|
|||
self.feature.required.discard(x)
|
||||
_dbg("Disabling %s", x)
|
||||
if getattr(self, f"enable_{x}"):
|
||||
msg = f"Conflicting options: --enable-{x} and --disable-{x}"
|
||||
msg = f"Conflicting options: '-C {x}=enable' and '-C {x}=disable'"
|
||||
raise ValueError(msg)
|
||||
if x == "freetype":
|
||||
_dbg("--disable-freetype implies --disable-raqm")
|
||||
_dbg("'-C freetype=disable' implies '-C raqm=disable'")
|
||||
if getattr(self, "enable_raqm"):
|
||||
msg = (
|
||||
"Conflicting options: --enable-raqm and --disable-freetype"
|
||||
"Conflicting options: "
|
||||
"'-C raqm=enable' and '-C freetype=disable'"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
setattr(self, "disable_raqm", True)
|
||||
|
@ -410,15 +411,17 @@ class pil_build_ext(build_ext):
|
|||
_dbg("Requiring %s", x)
|
||||
self.feature.required.add(x)
|
||||
if x == "raqm":
|
||||
_dbg("--enable-raqm implies --enable-freetype")
|
||||
_dbg("'-C raqm=enable' implies '-C freetype=enable'")
|
||||
self.feature.required.add("freetype")
|
||||
for x in ("raqm", "fribidi"):
|
||||
if getattr(self, f"vendor_{x}"):
|
||||
if getattr(self, "disable_raqm"):
|
||||
msg = f"Conflicting options: --vendor-{x} and --disable-raqm"
|
||||
msg = f"Conflicting options: '-C {x}=vendor' and '-C raqm=disable'"
|
||||
raise ValueError(msg)
|
||||
if x == "fribidi" and not getattr(self, "vendor_raqm"):
|
||||
msg = f"Conflicting options: --vendor-{x} and not --vendor-raqm"
|
||||
msg = (
|
||||
f"Conflicting options: '-C {x}=vendor' and not '-C raqm=vendor'"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
_dbg("Using vendored version of %s", x)
|
||||
self.feature.vendor.add(x)
|
||||
|
@ -451,7 +454,7 @@ class pil_build_ext(build_ext):
|
|||
def get_macos_sdk_path(self) -> str | None:
|
||||
try:
|
||||
sdk_path = (
|
||||
subprocess.check_output(["xcrun", "--show-sdk-path"])
|
||||
subprocess.check_output(["xcrun", "--show-sdk-path", "--sdk", "macosx"])
|
||||
.strip()
|
||||
.decode("latin1")
|
||||
)
|
||||
|
@ -609,6 +612,7 @@ class pil_build_ext(build_ext):
|
|||
_add_directory(library_dirs, "/usr/X11/lib")
|
||||
_add_directory(include_dirs, "/usr/X11/include")
|
||||
|
||||
# Add the macOS SDK path.
|
||||
sdk_path = self.get_macos_sdk_path()
|
||||
if sdk_path:
|
||||
_add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib"))
|
||||
|
@ -693,6 +697,8 @@ class pil_build_ext(build_ext):
|
|||
feature.set("zlib", "z")
|
||||
elif sys.platform == "win32" and _find_library_file(self, "zlib"):
|
||||
feature.set("zlib", "zlib") # alternative name
|
||||
elif sys.platform == "win32" and _find_library_file(self, "zdll"):
|
||||
feature.set("zlib", "zdll") # dll import library
|
||||
|
||||
if feature.want("jpeg"):
|
||||
_dbg("Looking for jpeg")
|
||||
|
@ -1052,7 +1058,7 @@ except DependencyException as err:
|
|||
msg = f"""
|
||||
|
||||
The headers or library files could not be found for {str(err)},
|
||||
which was requested by the option flag --enable-{str(err)}
|
||||
which was requested by the option flag '-C {str(err)}=enable'
|
||||
|
||||
"""
|
||||
sys.stderr.write(msg)
|
||||
|
|
|
@ -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, 0, 1))]
|
||||
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))
|
||||
|
|
|
@ -560,9 +560,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
|||
+ 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))]
|
||||
)
|
||||
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])
|
||||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
|
|
|
@ -454,7 +454,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -
|
|||
if hasattr(fp, "flush"):
|
||||
fp.flush()
|
||||
|
||||
ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0, None)])
|
||||
ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)])
|
||||
|
||||
fp.write(b"\n%%%%EndBinary\n")
|
||||
fp.write(b"grestore end\n")
|
||||
|
|
|
@ -303,38 +303,38 @@ TAGS = {
|
|||
|
||||
|
||||
class GPS(IntEnum):
|
||||
GPSVersionID = 0
|
||||
GPSLatitudeRef = 1
|
||||
GPSLatitude = 2
|
||||
GPSLongitudeRef = 3
|
||||
GPSLongitude = 4
|
||||
GPSAltitudeRef = 5
|
||||
GPSAltitude = 6
|
||||
GPSTimeStamp = 7
|
||||
GPSSatellites = 8
|
||||
GPSStatus = 9
|
||||
GPSMeasureMode = 10
|
||||
GPSDOP = 11
|
||||
GPSSpeedRef = 12
|
||||
GPSSpeed = 13
|
||||
GPSTrackRef = 14
|
||||
GPSTrack = 15
|
||||
GPSImgDirectionRef = 16
|
||||
GPSImgDirection = 17
|
||||
GPSMapDatum = 18
|
||||
GPSDestLatitudeRef = 19
|
||||
GPSDestLatitude = 20
|
||||
GPSDestLongitudeRef = 21
|
||||
GPSDestLongitude = 22
|
||||
GPSDestBearingRef = 23
|
||||
GPSDestBearing = 24
|
||||
GPSDestDistanceRef = 25
|
||||
GPSDestDistance = 26
|
||||
GPSProcessingMethod = 27
|
||||
GPSAreaInformation = 28
|
||||
GPSDateStamp = 29
|
||||
GPSDifferential = 30
|
||||
GPSHPositioningError = 31
|
||||
GPSVersionID = 0x00
|
||||
GPSLatitudeRef = 0x01
|
||||
GPSLatitude = 0x02
|
||||
GPSLongitudeRef = 0x03
|
||||
GPSLongitude = 0x04
|
||||
GPSAltitudeRef = 0x05
|
||||
GPSAltitude = 0x06
|
||||
GPSTimeStamp = 0x07
|
||||
GPSSatellites = 0x08
|
||||
GPSStatus = 0x09
|
||||
GPSMeasureMode = 0x0A
|
||||
GPSDOP = 0x0B
|
||||
GPSSpeedRef = 0x0C
|
||||
GPSSpeed = 0x0D
|
||||
GPSTrackRef = 0x0E
|
||||
GPSTrack = 0x0F
|
||||
GPSImgDirectionRef = 0x10
|
||||
GPSImgDirection = 0x11
|
||||
GPSMapDatum = 0x12
|
||||
GPSDestLatitudeRef = 0x13
|
||||
GPSDestLatitude = 0x14
|
||||
GPSDestLongitudeRef = 0x15
|
||||
GPSDestLongitude = 0x16
|
||||
GPSDestBearingRef = 0x17
|
||||
GPSDestBearing = 0x18
|
||||
GPSDestDistanceRef = 0x19
|
||||
GPSDestDistance = 0x1A
|
||||
GPSProcessingMethod = 0x1B
|
||||
GPSAreaInformation = 0x1C
|
||||
GPSDateStamp = 0x1D
|
||||
GPSDifferential = 0x1E
|
||||
GPSHPositioningError = 0x1F
|
||||
|
||||
|
||||
"""Maps EXIF GPS tags to tag names."""
|
||||
|
@ -342,40 +342,41 @@ GPSTAGS = {i.value: i.name for i in GPS}
|
|||
|
||||
|
||||
class Interop(IntEnum):
|
||||
InteropIndex = 1
|
||||
InteropVersion = 2
|
||||
RelatedImageFileFormat = 4096
|
||||
RelatedImageWidth = 4097
|
||||
RelatedImageHeight = 4098
|
||||
InteropIndex = 0x0001
|
||||
InteropVersion = 0x0002
|
||||
RelatedImageFileFormat = 0x1000
|
||||
RelatedImageWidth = 0x1001
|
||||
RelatedImageHeight = 0x1002
|
||||
|
||||
|
||||
class IFD(IntEnum):
|
||||
Exif = 34665
|
||||
GPSInfo = 34853
|
||||
Makernote = 37500
|
||||
Interop = 40965
|
||||
Exif = 0x8769
|
||||
GPSInfo = 0x8825
|
||||
MakerNote = 0x927C
|
||||
Makernote = 0x927C # Deprecated
|
||||
Interop = 0xA005
|
||||
IFD1 = -1
|
||||
|
||||
|
||||
class LightSource(IntEnum):
|
||||
Unknown = 0
|
||||
Daylight = 1
|
||||
Fluorescent = 2
|
||||
Tungsten = 3
|
||||
Flash = 4
|
||||
Fine = 9
|
||||
Cloudy = 10
|
||||
Shade = 11
|
||||
DaylightFluorescent = 12
|
||||
DayWhiteFluorescent = 13
|
||||
CoolWhiteFluorescent = 14
|
||||
WhiteFluorescent = 15
|
||||
StandardLightA = 17
|
||||
StandardLightB = 18
|
||||
StandardLightC = 19
|
||||
D55 = 20
|
||||
D65 = 21
|
||||
D75 = 22
|
||||
D50 = 23
|
||||
ISO = 24
|
||||
Other = 255
|
||||
Unknown = 0x00
|
||||
Daylight = 0x01
|
||||
Fluorescent = 0x02
|
||||
Tungsten = 0x03
|
||||
Flash = 0x04
|
||||
Fine = 0x09
|
||||
Cloudy = 0x0A
|
||||
Shade = 0x0B
|
||||
DaylightFluorescent = 0x0C
|
||||
DayWhiteFluorescent = 0x0D
|
||||
CoolWhiteFluorescent = 0x0E
|
||||
WhiteFluorescent = 0x0F
|
||||
StandardLightA = 0x11
|
||||
StandardLightB = 0x12
|
||||
StandardLightC = 0x13
|
||||
D55 = 0x14
|
||||
D65 = 0x15
|
||||
D75 = 0x16
|
||||
D50 = 0x17
|
||||
ISO = 0x18
|
||||
Other = 0xFF
|
||||
|
|
|
@ -159,7 +159,7 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
framesize = i32(s)
|
||||
|
||||
self.decodermaxblock = framesize
|
||||
self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset, None)]
|
||||
self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)]
|
||||
|
||||
self.__offset += framesize
|
||||
|
||||
|
|
|
@ -170,7 +170,7 @@ class FpxImageFile(ImageFile.ImageFile):
|
|||
"raw",
|
||||
(x, y, x1, y1),
|
||||
i32(s, i) + 28,
|
||||
(self.rawmode,),
|
||||
self.rawmode,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -95,7 +95,7 @@ class FtexImageFile(ImageFile.ImageFile):
|
|||
self._mode = "RGBA"
|
||||
self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))]
|
||||
elif format == Format.UNCOMPRESSED:
|
||||
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
|
||||
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")]
|
||||
else:
|
||||
msg = f"Invalid texture compression format: {repr(format)}"
|
||||
raise ValueError(msg)
|
||||
|
|
|
@ -76,7 +76,7 @@ class GdImageFile(ImageFile.ImageFile):
|
|||
"raw",
|
||||
(0, 0) + self.size,
|
||||
7 + true_color_offset + 4 + 256 * 4,
|
||||
("L", 0, 1),
|
||||
"L",
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
@ -695,8 +695,9 @@ def _write_multiple_frames(
|
|||
)
|
||||
background = _get_background(im_frame, color)
|
||||
background_im = Image.new("P", im_frame.size, background)
|
||||
assert im_frames[0].im.palette is not None
|
||||
background_im.putpalette(im_frames[0].im.palette)
|
||||
first_palette = im_frames[0].im.palette
|
||||
assert first_palette is not None
|
||||
background_im.putpalette(first_palette, first_palette.mode)
|
||||
bbox = _getbbox(background_im, im_frame)[1]
|
||||
elif encoderinfo.get("optimize") and im_frame.mode != "1":
|
||||
if "transparency" not in encoderinfo:
|
||||
|
|
|
@ -357,7 +357,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
|||
name = "".join([name[: 92 - len(ext)], ext])
|
||||
|
||||
fp.write(f"Name: {name}\r\n".encode("ascii"))
|
||||
fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode("ascii"))
|
||||
fp.write(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii"))
|
||||
fp.write(f"File size (no of images): {frames}\r\n".encode("ascii"))
|
||||
if im.mode in ["P", "PA"]:
|
||||
fp.write(b"Lut: 1\r\n")
|
||||
|
|
114
src/PIL/Image.py
114
src/PIL/Image.py
|
@ -603,24 +603,16 @@ class Image:
|
|||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def _close_fp(self):
|
||||
if getattr(self, "_fp", False):
|
||||
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
|
||||
|
||||
|
@ -692,13 +677,10 @@ class Image:
|
|||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % (
|
||||
self.__class__.__module__,
|
||||
self.__class__.__name__,
|
||||
self.mode,
|
||||
self.size[0],
|
||||
self.size[1],
|
||||
id(self),
|
||||
return (
|
||||
f"<{self.__class__.__module__}.{self.__class__.__name__} "
|
||||
f"image mode={self.mode} size={self.size[0]}x{self.size[1]} "
|
||||
f"at 0x{id(self):X}>"
|
||||
)
|
||||
|
||||
def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None:
|
||||
|
@ -707,14 +689,8 @@ class Image:
|
|||
# Same as __repr__ but without unpredictable id(self),
|
||||
# to keep Jupyter notebook `text/plain` output stable.
|
||||
p.text(
|
||||
"<%s.%s image mode=%s size=%dx%d>"
|
||||
% (
|
||||
self.__class__.__module__,
|
||||
self.__class__.__name__,
|
||||
self.mode,
|
||||
self.size[0],
|
||||
self.size[1],
|
||||
)
|
||||
f"<{self.__class__.__module__}.{self.__class__.__name__} "
|
||||
f"image mode={self.mode} size={self.size[0]}x{self.size[1]}>"
|
||||
)
|
||||
|
||||
def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None:
|
||||
|
@ -763,7 +739,7 @@ class Image:
|
|||
|
||||
def __setstate__(self, state: list[Any]) -> None:
|
||||
Image.__init__(self)
|
||||
info, mode, size, palette, data = state
|
||||
info, mode, size, palette, data = state[:5]
|
||||
self.info = info
|
||||
self._mode = mode
|
||||
self._size = size
|
||||
|
@ -1563,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(513):
|
||||
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(513)
|
||||
if thumbnail_offset is not None:
|
||||
thumbnail_offset += getattr(self, "_exif_offset", 0)
|
||||
self.fp.seek(thumbnail_offset)
|
||||
data = self.fp.read(ifd.get(514))
|
||||
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:
|
||||
"""
|
||||
|
@ -2550,7 +2486,7 @@ class Image:
|
|||
filename: str | bytes = ""
|
||||
open_fp = False
|
||||
if is_path(fp):
|
||||
filename = os.path.realpath(os.fspath(fp))
|
||||
filename = os.fspath(fp)
|
||||
open_fp = True
|
||||
elif fp == sys.stdout:
|
||||
try:
|
||||
|
@ -2559,13 +2495,13 @@ class Image:
|
|||
pass
|
||||
if not filename and hasattr(fp, "name") and is_path(fp.name):
|
||||
# only set the name for metadata purposes
|
||||
filename = os.path.realpath(os.fspath(fp.name))
|
||||
filename = os.fspath(fp.name)
|
||||
|
||||
# may mutate self!
|
||||
self._ensure_mutable()
|
||||
|
||||
save_all = params.pop("save_all", False)
|
||||
self.encoderinfo = params
|
||||
self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params}
|
||||
self.encoderconfig: tuple[Any, ...] = ()
|
||||
|
||||
preinit()
|
||||
|
@ -2612,6 +2548,11 @@ class Image:
|
|||
except PermissionError:
|
||||
pass
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
del self.encoderinfo
|
||||
except AttributeError:
|
||||
pass
|
||||
if open_fp:
|
||||
fp.close()
|
||||
|
||||
|
@ -3463,7 +3404,7 @@ def open(
|
|||
exclusive_fp = False
|
||||
filename: str | bytes = ""
|
||||
if is_path(fp):
|
||||
filename = os.path.realpath(os.fspath(fp))
|
||||
filename = os.fspath(fp)
|
||||
|
||||
if filename:
|
||||
fp = builtins.open(filename, "rb")
|
||||
|
@ -3893,7 +3834,7 @@ class Exif(_ExifBase):
|
|||
gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo)
|
||||
print(gps_ifd)
|
||||
|
||||
Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``,
|
||||
Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.MakerNote``,
|
||||
``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``.
|
||||
|
||||
:py:mod:`~PIL.ExifTags` also has enum classes to provide names for data::
|
||||
|
@ -4027,6 +3968,9 @@ class Exif(_ExifBase):
|
|||
|
||||
head = self._get_head()
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
|
||||
for tag, ifd_dict in self._ifds.items():
|
||||
if tag not in self:
|
||||
ifd[tag] = ifd_dict
|
||||
for tag, value in self.items():
|
||||
if tag in [
|
||||
ExifTags.IFD.Exif,
|
||||
|
@ -4056,11 +4000,11 @@ class Exif(_ExifBase):
|
|||
ifd = self._get_ifd_dict(offset, tag)
|
||||
if ifd is not None:
|
||||
self._ifds[tag] = ifd
|
||||
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]:
|
||||
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.MakerNote]:
|
||||
if ExifTags.IFD.Exif not in self._ifds:
|
||||
self.get_ifd(ExifTags.IFD.Exif)
|
||||
tag_data = self._ifds[ExifTags.IFD.Exif][tag]
|
||||
if tag == ExifTags.IFD.Makernote:
|
||||
if tag == ExifTags.IFD.MakerNote:
|
||||
from .TiffImagePlugin import ImageFileDirectory_v2
|
||||
|
||||
if tag_data[:8] == b"FUJIFILM":
|
||||
|
@ -4147,7 +4091,7 @@ class Exif(_ExifBase):
|
|||
ifd = {
|
||||
k: v
|
||||
for (k, v) in ifd.items()
|
||||
if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote)
|
||||
if k not in (ExifTags.IFD.Interop, ExifTags.IFD.MakerNote)
|
||||
}
|
||||
return ifd
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -98,8 +101,8 @@ def _tilesort(t: _Tile) -> int:
|
|||
class _Tile(NamedTuple):
|
||||
codec_name: str
|
||||
extents: tuple[int, int, int, int] | None
|
||||
offset: int
|
||||
args: tuple[Any, ...] | str | None
|
||||
offset: int = 0
|
||||
args: tuple[Any, ...] | str | None = None
|
||||
|
||||
|
||||
#
|
||||
|
@ -120,7 +123,7 @@ class ImageFile(Image.Image):
|
|||
self.custom_mimetype: str | None = None
|
||||
|
||||
self.tile: list[_Tile] = []
|
||||
""" A list of tile descriptors, or ``None`` """
|
||||
""" A list of tile descriptors """
|
||||
|
||||
self.readonly = 1 # until we know better
|
||||
|
||||
|
@ -130,7 +133,7 @@ class ImageFile(Image.Image):
|
|||
if is_path(fp):
|
||||
# filename
|
||||
self.fp = open(fp, "rb")
|
||||
self.filename = os.path.realpath(os.fspath(fp))
|
||||
self.filename = os.fspath(fp)
|
||||
self._exclusive_fp = True
|
||||
else:
|
||||
# stream
|
||||
|
@ -163,6 +166,85 @@ class ImageFile(Image.Image):
|
|||
def _open(self) -> None:
|
||||
pass
|
||||
|
||||
def _close_fp(self):
|
||||
if getattr(self, "_fp", False):
|
||||
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
|
||||
|
|
|
@ -553,7 +553,7 @@ class Color3DLUT(MultibandFilter):
|
|||
ch_out = channels or ch_in
|
||||
size_1d, size_2d, size_3d = self.size
|
||||
|
||||
table = [0] * (size_1d * size_2d * size_3d * ch_out)
|
||||
table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out)
|
||||
idx_in = 0
|
||||
idx_out = 0
|
||||
for b in range(size_3d):
|
||||
|
|
|
@ -270,7 +270,7 @@ class FreeTypeFont:
|
|||
)
|
||||
|
||||
if is_path(font):
|
||||
font = os.path.realpath(os.fspath(font))
|
||||
font = os.fspath(font)
|
||||
if sys.platform == "win32":
|
||||
font_bytes_path = font if isinstance(font, bytes) else font.encode()
|
||||
try:
|
||||
|
|
|
@ -104,28 +104,17 @@ def grab(
|
|||
|
||||
def grabclipboard() -> Image.Image | list[str] | None:
|
||||
if sys.platform == "darwin":
|
||||
fh, filepath = tempfile.mkstemp(".png")
|
||||
os.close(fh)
|
||||
commands = [
|
||||
'set theFile to (open for access POSIX file "'
|
||||
+ filepath
|
||||
+ '" with write permission)',
|
||||
"try",
|
||||
" write (the clipboard as «class PNGf») to theFile",
|
||||
"end try",
|
||||
"close access theFile",
|
||||
]
|
||||
script = ["osascript"]
|
||||
for command in commands:
|
||||
script += ["-e", command]
|
||||
subprocess.call(script)
|
||||
p = subprocess.run(
|
||||
["osascript", "-e", "get the clipboard as «class PNGf»"],
|
||||
capture_output=True,
|
||||
)
|
||||
if p.returncode != 0:
|
||||
return None
|
||||
|
||||
im = None
|
||||
if os.stat(filepath).st_size != 0:
|
||||
im = Image.open(filepath)
|
||||
im.load()
|
||||
os.unlink(filepath)
|
||||
return im
|
||||
import binascii
|
||||
|
||||
data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3]))
|
||||
return Image.open(data)
|
||||
elif sys.platform == "win32":
|
||||
fmt, data = Image.core.grabclipboard_win32()
|
||||
if fmt == "file": # CF_HDROP
|
||||
|
|
|
@ -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
|
||||
|
@ -698,10 +708,11 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
|
|||
8: Image.Transpose.ROTATE_90,
|
||||
}.get(orientation)
|
||||
if method is not None:
|
||||
transposed_image = image.transpose(method)
|
||||
if in_place:
|
||||
image.im = transposed_image.im
|
||||
image._size = transposed_image._size
|
||||
image.im = image.im.transpose(method)
|
||||
image._size = image.im.size
|
||||
else:
|
||||
transposed_image = image.transpose(method)
|
||||
exif_image = image if in_place else transposed_image
|
||||
|
||||
exif = exif_image.getexif()
|
||||
|
|
|
@ -62,7 +62,7 @@ class ImtImageFile(ImageFile.ImageFile):
|
|||
"raw",
|
||||
(0, 0) + self.size,
|
||||
self.fp.tell() - len(buffer),
|
||||
(self.mode, 0, 1),
|
||||
self.mode,
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
@ -252,6 +252,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
|
|||
if sig == b"\xff\x4f\xff\x51":
|
||||
self.codec = "j2k"
|
||||
self._size, self._mode = _parse_codestream(self.fp)
|
||||
self._parse_comment()
|
||||
else:
|
||||
sig = sig + self.fp.read(8)
|
||||
|
||||
|
@ -262,6 +263,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
|
|||
if dpi is not None:
|
||||
self.info["dpi"] = dpi
|
||||
if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"):
|
||||
hdr = self.fp.read(2)
|
||||
length = _binary.i16be(hdr)
|
||||
self.fp.seek(length - 2, os.SEEK_CUR)
|
||||
self._parse_comment()
|
||||
else:
|
||||
msg = "not a JPEG 2000 file"
|
||||
|
@ -296,10 +300,6 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
|
|||
]
|
||||
|
||||
def _parse_comment(self) -> None:
|
||||
hdr = self.fp.read(2)
|
||||
length = _binary.i16be(hdr)
|
||||
self.fp.seek(length - 2, os.SEEK_CUR)
|
||||
|
||||
while True:
|
||||
marker = self.fp.read(2)
|
||||
if not marker:
|
||||
|
|
|
@ -72,7 +72,7 @@ def APP(self: JpegImageFile, marker: int) -> None:
|
|||
n = i16(self.fp.read(2)) - 2
|
||||
s = ImageFile._safe_read(self.fp, n)
|
||||
|
||||
app = "APP%d" % (marker & 15)
|
||||
app = f"APP{marker & 15}"
|
||||
|
||||
self.app[app] = s # compatibility
|
||||
self.applist.append((app, s))
|
||||
|
@ -90,6 +90,9 @@ def APP(self: JpegImageFile, marker: int) -> None:
|
|||
else:
|
||||
if jfif_unit == 1:
|
||||
self.info["dpi"] = jfif_density
|
||||
elif jfif_unit == 2: # cm
|
||||
# 1 dpcm = 2.54 dpi
|
||||
self.info["dpi"] = tuple(d * 2.54 for d in jfif_density)
|
||||
self.info["jfif_unit"] = jfif_unit
|
||||
self.info["jfif_density"] = jfif_density
|
||||
elif marker == 0xFFE1 and s[:6] == b"Exif\0\0":
|
||||
|
@ -395,6 +398,13 @@ class JpegImageFile(ImageFile.ImageFile):
|
|||
return getattr(self, "_" + name)
|
||||
raise AttributeError(name)
|
||||
|
||||
def __getstate__(self) -> list[Any]:
|
||||
return super().__getstate__() + [self.layers, self.layer]
|
||||
|
||||
def __setstate__(self, state: list[Any]) -> None:
|
||||
super().__setstate__(state)
|
||||
self.layers, self.layer = state[5:]
|
||||
|
||||
def load_read(self, read_bytes: int) -> bytes:
|
||||
"""
|
||||
internal: read more image data
|
||||
|
@ -751,7 +761,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
|||
extra = info.get("extra", b"")
|
||||
|
||||
MAX_BYTES_IN_MARKER = 65533
|
||||
xmp = info.get("xmp", im.info.get("xmp"))
|
||||
xmp = info.get("xmp")
|
||||
if xmp:
|
||||
overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
|
||||
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
|
||||
|
|
|
@ -70,9 +70,9 @@ class MspImageFile(ImageFile.ImageFile):
|
|||
self._size = i16(s, 4), i16(s, 6)
|
||||
|
||||
if s[:4] == b"DanM":
|
||||
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))]
|
||||
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")]
|
||||
else:
|
||||
self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32, None)]
|
||||
self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)]
|
||||
|
||||
|
||||
class MspDecoder(ImageFile.PyDecoder):
|
||||
|
@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
|||
fp.write(o16(h))
|
||||
|
||||
# image body
|
||||
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, ("1", 0, 1))])
|
||||
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")])
|
||||
|
||||
|
||||
#
|
||||
|
|
|
@ -47,7 +47,7 @@ class PcdImageFile(ImageFile.ImageFile):
|
|||
|
||||
self._mode = "RGB"
|
||||
self._size = 768, 512 # FIXME: not correct for rotated images!
|
||||
self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048, None)]
|
||||
self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)]
|
||||
|
||||
def load_end(self) -> None:
|
||||
if self.tile_post_rotate:
|
||||
|
|
|
@ -86,7 +86,7 @@ class PcxImageFile(ImageFile.ImageFile):
|
|||
|
||||
elif bits == 1 and planes in (2, 4):
|
||||
mode = "P"
|
||||
rawmode = "P;%dL" % planes
|
||||
rawmode = f"P;{planes}L"
|
||||
self.palette = ImagePalette.raw("RGB", s[16:64])
|
||||
|
||||
elif version == 5 and bits == 8 and planes == 1:
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user