Merge branch 'main' into load_default

This commit is contained in:
Andrew Murray 2023-10-14 10:33:50 +11:00
commit 0da7ad7c48
134 changed files with 2301 additions and 468 deletions

View File

@ -21,13 +21,11 @@ environment:
install: install:
- '%PYTHON%\%EXECUTABLE% --version' - '%PYTHON%\%EXECUTABLE% --version'
- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip' - '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
- 7z x pillow-depends.zip -oc:\
- 7z x pillow-test-images.zip -oc:\ - 7z x pillow-test-images.zip -oc:\
- mv c:\pillow-depends-main c:\pillow-depends
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
- 7z x ..\pillow-depends\nasm-2.16.01-win64.zip -oc:\ - curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip
- 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.0.0.20230317 - choco install ghostscript --version=10.0.0.20230317
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH% - path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
- cd c:\pillow\winbuild\ - cd c:\pillow\winbuild\

View File

@ -23,7 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard sway wl-clipboard libopenblas-dev
fi fi
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
@ -38,11 +38,10 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
# TODO Remove condition when NumPy supports 3.12 python3 -m pip install numpy
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ "$GHA_PYTHON_VERSION" != "3.12-dev" && $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
python3 -m pip install pyqt6 python3 -m pip install pyqt6
fi fi

View File

@ -28,7 +28,7 @@ jobs:
name: Docs name: Docs
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4

View File

@ -17,7 +17,7 @@ jobs:
name: Lint name: Lint
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: pre-commit cache - name: pre-commit cache
uses: actions/cache@v3 uses: actions/cache@v3

View File

@ -3,6 +3,7 @@
set -e set -e
brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
PYTHONOPTIMIZE=0 python3 -m pip install cffi PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install coverage python3 -m pip install coverage
@ -13,8 +14,7 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
# TODO Remove condition when NumPy supports 3.12 python3 -m pip install numpy
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -10,7 +10,7 @@ on:
permissions: permissions:
contents: read contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true

View File

@ -8,7 +8,7 @@ on:
permissions: permissions:
issues: write issues: write
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true

View File

@ -4,11 +4,19 @@ on:
push: push:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
pull_request: pull_request:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -36,7 +44,7 @@ jobs:
git config --global core.autocrlf input git config --global core.autocrlf input
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install Cygwin - name: Install Cygwin
uses: cygwin/cygwin-install-action@v4 uses: cygwin/cygwin-install-action@v4
@ -102,10 +110,10 @@ jobs:
run: | run: |
bash.exe .ci/install.sh bash.exe .ci/install.sh
- name: Install latest NumPy - name: Upgrade NumPy
shell: dash.exe -l "{0}" shell: dash.exe -l "{0}"
run: | run: |
python3 -m pip install -U numpy python3 -m pip install -U "numpy<1.26"
- name: Build - name: Build
shell: bash.exe -eo pipefail -o igncr "{0}" shell: bash.exe -eo pipefail -o igncr "{0}"

View File

@ -4,11 +4,19 @@ on:
push: push:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
pull_request: pull_request:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -59,7 +67,7 @@ jobs:
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py

View File

@ -4,11 +4,19 @@ on:
push: push:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
pull_request: pull_request:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -34,7 +42,7 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up shell - name: Set up shell
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH

View File

@ -37,7 +37,7 @@ jobs:
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py

View File

@ -4,11 +4,19 @@ on:
push: push:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
pull_request: pull_request:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -24,7 +32,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12"]
timeout-minutes: 30 timeout-minutes: 30
@ -32,16 +40,16 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Checkout cached dependencies - name: Checkout cached dependencies
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
repository: python-pillow/pillow-depends repository: python-pillow/pillow-depends
path: winbuild\depends path: winbuild\depends
- name: Checkout extra test images - name: Checkout extra test images
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
repository: python-pillow/test-images repository: python-pillow/test-images
path: Tests\test-images path: Tests\test-images

View File

@ -4,11 +4,19 @@ on:
push: push:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
pull_request: pull_request:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -31,7 +39,7 @@ jobs:
python-version: [ python-version: [
"pypy3.10", "pypy3.10",
"pypy3.9", "pypy3.9",
"3.12-dev", "3.12",
"3.11", "3.11",
"3.10", "3.10",
"3.9", "3.9",
@ -48,7 +56,7 @@ jobs:
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4 uses: actions/setup-python@v4

40
.github/workflows/wheels-build.sh vendored Executable file
View File

@ -0,0 +1,40 @@
#!/bin/bash
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
# webp, zstd, xz, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
# libxdmcp causes an issue on macOS < 11
# curl from brew requires zstd, use system curl
# if php is installed, brew tries to reinstall these after installing openblas
# remove lcms2 and libpng to fix building openjpeg on arm64
brew remove --ignore-dependencies webp zstd xz libpng libtiff libxcb libxdmcp curl php lcms2 ghostscript
brew install pkg-config
if [[ "$PLAT" == "arm64" ]]; then
export MACOSX_DEPLOYMENT_TARGET="11.0"
else
export MACOSX_DEPLOYMENT_TARGET="10.10"
fi
fi
if [[ "$MB_PYTHON_VERSION" == pypy3* ]]; then
MB_PYTHON_OSX_VER="10.9"
fi
echo "::group::Install a virtualenv"
source wheels/multibuild/common_utils.sh
source wheels/multibuild/travis_steps.sh
python3 -m pip install virtualenv
before_install
echo "::endgroup::"
echo "::group::Build wheel"
build_wheel
ls -l "${GITHUB_WORKSPACE}/${WHEEL_SDIR}/"
echo "::endgroup::"
if [[ $MACOSX_DEPLOYMENT_TARGET != "11.0" ]]; then
echo "::group::Test wheel"
install_run
echo "::endgroup::"
fi

69
.github/workflows/wheels-linux.yml vendored Normal file
View File

@ -0,0 +1,69 @@
name: Build Linux wheels
on:
workflow_call:
inputs:
artifacts-name:
required: true
type: string
env:
CONFIG_PATH: "wheels/config.sh"
REPO_DIR: "."
TEST_DEPENDS: "pytest pytest-timeout"
jobs:
build:
name: ${{ matrix.python }} ${{ matrix.mb-ml-libc }}${{ matrix.mb-ml-ver }}
runs-on: "ubuntu-latest"
strategy:
fail-fast: false
matrix:
python: [
"pypy3.9-7.3.13",
"pypy3.10-7.3.13",
"3.8",
"3.9",
"3.10",
"3.11",
"3.12",
]
mb-ml-libc: [ "manylinux" ]
mb-ml-ver: [ 2014, "_2_28" ]
include:
- python: "3.8"
mb-ml-libc: "musllinux"
mb-ml-ver: "_1_1"
- python: "3.9"
mb-ml-libc: "musllinux"
mb-ml-ver: "_1_1"
- python: "3.10"
mb-ml-libc: "musllinux"
mb-ml-ver: "_1_1"
- python: "3.11"
mb-ml-libc: "musllinux"
mb-ml-ver: "_1_1"
- python: "3.12"
mb-ml-libc: "musllinux"
mb-ml-ver: "_1_1"
env:
MB_PYTHON_VERSION: ${{ matrix.python }}
MB_ML_LIBC: ${{ matrix.mb-ml-libc }}
MB_ML_VER: ${{ matrix.mb-ml-ver }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Build Wheel
run: .github/workflows/wheels-build.sh
- uses: actions/upload-artifact@v3
with:
name: ${{ inputs.artifacts-name }}
path: wheelhouse/*.whl
# Uncomment to get SSH access for testing
# - name: Setup tmate session
# if: failure()
# uses: mxschmitt/action-tmate@v3

57
.github/workflows/wheels-macos.yml vendored Normal file
View File

@ -0,0 +1,57 @@
name: Build macOS wheels
on:
workflow_call:
inputs:
artifacts-name:
required: true
type: string
env:
CONFIG_PATH: "wheels/config.sh"
REPO_DIR: "."
TEST_DEPENDS: "pytest pytest-timeout"
jobs:
build:
name: ${{ matrix.python }} ${{ matrix.platform }}
runs-on: "macos-latest"
strategy:
fail-fast: false
matrix:
python: [
"pypy3.9-7.3.13",
"pypy3.10-7.3.13",
"3.8",
"3.9",
"3.10",
"3.11",
"3.12",
]
platform: [ "x86_64", "arm64" ]
exclude:
- python: "pypy3.9-7.3.13"
platform: "arm64"
- python: "pypy3.10-7.3.13"
platform: "arm64"
env:
PLAT: ${{ matrix.platform }}
MB_PYTHON_VERSION: ${{ matrix.python }}
TRAVIS_OS_NAME: "osx"
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Build Wheel
run: .github/workflows/wheels-build.sh
- uses: actions/upload-artifact@v3
with:
name: ${{ inputs.artifacts-name }}
path: wheelhouse/*.whl
# Uncomment to get SSH access for testing
# - name: Setup tmate session
# if: failure()
# uses: mxschmitt/action-tmate@v3

42
.github/workflows/wheels.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Wheels
on:
push:
paths:
- ".github/workflows/wheels*.yml"
- "wheels/*"
tags:
- "*"
pull_request:
paths:
- ".github/workflows/wheels*.yml"
- "wheels/*"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
macos:
uses: ./.github/workflows/wheels-macos.yml
with:
artifacts-name: "wheels"
linux:
uses: ./.github/workflows/wheels-linux.yml
with:
artifacts-name: "wheels"
success:
permissions:
contents: none
needs: [macos, linux]
runs-on: ubuntu-latest
name: Wheels Successful
steps:
- name: Success
run: echo Wheels Successful

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "multibuild"]
path = wheels/multibuild
url = https://github.com/multi-build/multibuild.git

View File

@ -1,6 +1,12 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/asottile/pyupgrade
rev: 23.7.0 rev: v3.13.0
hooks:
- id: pyupgrade
args: [--py38-plus]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.9.1
hooks: hooks:
- id: black - id: black
args: [--target-version=py38] args: [--target-version=py38]
@ -23,7 +29,7 @@ repos:
- id: yesqa - id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.3 rev: v1.5.4
hooks: hooks:
- id: remove-tabs - id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
@ -33,7 +39,7 @@ repos:
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: additional_dependencies:
[flake8-2020, flake8-errmsg, flake8-implicit-str-concat] [flake8-2020, flake8-errmsg, flake8-implicit-str-concat, flake8-logging]
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0 rev: v1.10.0
@ -44,23 +50,28 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.4.0
hooks: hooks:
- id: check-executables-have-shebangs
- id: check-merge-conflict - id: check-merge-conflict
- id: check-json - id: check-json
- id: check-toml - id: check-toml
- id: check-yaml - id: check-yaml
- id: end-of-file-fixer
exclude: ^Tests/images/
- id: trailing-whitespace
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.6.7 rev: v0.6.8
hooks: hooks:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: 0.13.0 rev: 1.2.0
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.13 rev: v0.14
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject

135
.travis.yml Normal file
View File

@ -0,0 +1,135 @@
if: tag IS present OR type = api
env:
global:
- CONFIG_PATH=wheels/config.sh
- REPO_DIR=.
- PLAT=aarch64
- TEST_DEPENDS=pytest-timeout
language: python
# Default Python version is usually 3.6
python: "3.11"
dist: focal
services: docker
jobs:
include:
- name: "3.8 Focal manylinux2014 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.8
- name: "3.8 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.8
- name: "3.8 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.8
- name: "3.9 Focal manylinux2014 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.9
- name: "3.9 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.9
- name: "3.9 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.9
- name: "3.10 Focal manylinux2014 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.10
- name: "3.10 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.10
- name: "3.10 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.10
- name: "3.11 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.11
- name: "3.11 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.11
- name: "3.11 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.11
- name: "3.12 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.12
- name: "3.12 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.12
- name: "3.12 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.12
before_install:
- source wheels/multibuild/common_utils.sh
- source wheels/multibuild/travis_steps.sh
- before_install
install:
- build_multilinux aarch64 build_wheel
- ls -l "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/"
script:
- install_run
# Upload wheels to GitHub Releases
deploy:
provider: releases
api_key: $GITHUB_RELEASE_TOKEN
file_glob: true
file: "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/*.whl"
on:
repo: python-pillow/Pillow
tags: true
skip_cleanup: true

View File

@ -5,15 +5,78 @@ Changelog (Pillow)
10.1.0 (unreleased) 10.1.0 (unreleased)
------------------- -------------------
- Added ImageOps cover method #7412
[radarhere, hugovk]
- Catch struct.error from truncated EXIF when reading JPEG DPI #7458
[radarhere]
- Consider default image when selecting mode for PNG save_all #7437
[radarhere]
- Support BGR;15, BGR;16 and BGR;24 access, unpacking and putdata #7303
[radarhere]
- Added CMYK to RGB unpacker #7310
[radarhere]
- Improved flexibility of XMP parsing #7274
[radarhere]
- Support reading 8-bit YCbCr TIFF images #7415
[radarhere]
- Allow saving I;16B images as PNG #7302
[radarhere]
- Corrected drawing I;16 points and writing I;16 text #7257
[radarhere]
- Set blue channel to 128 for BC5S #7413
[radarhere]
- Increase flexibility when reading IPTC fields #7319
[radarhere]
- Set C palette to be empty by default #7289
[radarhere]
- Added gs_binary to control Ghostscript use on all platforms #7392
[radarhere]
- Read bounding box information from the trailer of EPS files if specified #7382
[nopperl, radarhere]
- Added reading 8-bit color DDS images #7426
[radarhere]
- Added has_transparency_data #7420
[radarhere, hugovk]
- Fixed bug when reading BC5S DDS images #7401
[radarhere]
- Prevent TIFF orientation from being applied more than once #7383
[radarhere]
- Use previous pixel alpha for QOI_OP_RGB #7357
[radarhere]
- Added BC5U reading #7358
[radarhere]
- Allow getpixel() to accept a list #7355
[radarhere, homm]
- Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii #7336 - Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii #7336
[radarhere] [radarhere]
- Expand JPEG buffer size when saving optimized or progressive #7345
[radarhere]
- Added session type check for Linux in ImageGrab.grabclipboard() #7332 - Added session type check for Linux in ImageGrab.grabclipboard() #7332
[TheNooB2706, radarhere, hugovk] [TheNooB2706, radarhere, hugovk]
- Read WebP duration after opening #7311
[k128, radarhere]
- Allow "loop=None" when saving GIF images #7329 - Allow "loop=None" when saving GIF images #7329
[radarhere] [radarhere]
@ -38,6 +101,15 @@ Changelog (Pillow)
- Fix missing symbols when libtiff depends on libjpeg #7270 - Fix missing symbols when libtiff depends on libjpeg #7270
[heitbaum] [heitbaum]
10.0.1 (2023-09-15)
-------------------
- Updated libwebp to 1.3.2 #7395
[radarhere]
- Updated zlib to 1.3 #7344
[radarhere]
10.0.0 (2023-07-01) 10.0.0 (2023-07-01)
------------------- -------------------
@ -5765,8 +5837,8 @@ http://svn.effbot.org/public/pil/
a polyline, independent of line angle. a polyline, independent of line angle.
- Fixed bearing calculation and clipping in the ImageFont truetype - Fixed bearing calculation and clipping in the ImageFont truetype
renderer; this could lead to clipped text, or crashes in the low- renderer; this could lead to clipped text, or crashes in the low-level
level _imagingft module. (based on input from Adam Twardoch and _imagingft module. (based on input from Adam Twardoch and
others). others).
- Added ImageQt wrapper module, for converting PIL Image objects to - Added ImageQt wrapper module, for converting PIL Image objects to
@ -5847,8 +5919,7 @@ http://svn.effbot.org/public/pil/
1.1.5c2 and 1.1.5 final 1.1.5c2 and 1.1.5 final
----------------------- -----------------------
- Added experimental PERSPECTIVE transform method (from Jeff Breiden- - Added experimental PERSPECTIVE transform method (from Jeff Breidenbach).
bach).
1.1.5c1 1.1.5c1
------- -------
@ -5920,8 +5991,8 @@ http://svn.effbot.org/public/pil/
- Fixed BILINEAR/BICUBIC/ANTIALIAS filtering for mode "LA". - Fixed BILINEAR/BICUBIC/ANTIALIAS filtering for mode "LA".
- Added "getcolors()" method. This is similar to the existing histo- - Added "getcolors()" method. This is similar to the existing histogram
gram method, but looks at color values instead of individual layers, method, but looks at color values instead of individual layers,
and returns an unsorted list of (count, color) tuples. and returns an unsorted list of (count, color) tuples.
By default, the method returns None if finds more than 256 colors. By default, the method returns None if finds more than 256 colors.
@ -6137,8 +6208,8 @@ http://svn.effbot.org/public/pil/
- Added limited support for "bitfield compression" in BMP files - Added limited support for "bitfield compression" in BMP files
and DIB buffers, for 15-bit, 16-bit, and 32-bit images. This and DIB buffers, for 15-bit, 16-bit, and 32-bit images. This
also fixes a problem with ImageGrab module when copying screen- also fixes a problem with ImageGrab module when copying screendumps
dumps from the clipboard on 15/16/32-bit displays. from the clipboard on 15/16/32-bit displays.
- Added experimental WAL (Quake 2 textures) loader. To use this - Added experimental WAL (Quake 2 textures) loader. To use this
loader, import WalImageFile and call the "open" method in that loader, import WalImageFile and call the "open" method in that
@ -6249,8 +6320,8 @@ http://svn.effbot.org/public/pil/
1.1.3 final 1.1.3 final
----------- -----------
- Made setup.py look for old versions of zlib. For some back- - Made setup.py look for old versions of zlib. For some background,
ground, see: https://zlib.net/advisory-2002-03-11.txt see: https://zlib.net/advisory-2002-03-11.txt
1.1.3c2 1.1.3c2
------- -------
@ -6441,8 +6512,8 @@ http://svn.effbot.org/public/pil/
supports all major PIL image modes (including F and I). supports all major PIL image modes (including F and I).
- The ImageFile module now includes a Parser class, which can - The ImageFile module now includes a Parser class, which can
be used to incrementally decode an image file (while down- be used to incrementally decode an image file (while downloading
loading it from the net, for example). See the handbook for it from the net, for example). See the handbook for
details. details.
- "show" now converts non-standard modes to "L" or "RGB" (as - "show" now converts non-standard modes to "L" or "RGB" (as
@ -6580,8 +6651,8 @@ http://svn.effbot.org/public/pil/
- The Image "transform" method now supports Image.QUAD transforms. - The Image "transform" method now supports Image.QUAD transforms.
The data argument is an 8-tuple giving the upper left, lower The data argument is an 8-tuple giving the upper left, lower
left, lower right, and upper right corner of the source quadri- left, lower right, and upper right corner of the source quadrilateral.
lateral. Also added Image.MESH transform which takes a list Also added Image.MESH transform which takes a list
of quadrilaterals. of quadrilaterals.
- The Image "resize", "rotate", and "transform" methods now support - The Image "resize", "rotate", and "transform" methods now support
@ -6806,8 +6877,8 @@ The test suite includes 400 individual tests.
neither "short", "int" nor "long" are 32-bit wide. neither "short", "int" nor "long" are 32-bit wide.
- Added file= and data= keyword arguments to PhotoImage and BitmapImage. - Added file= and data= keyword arguments to PhotoImage and BitmapImage.
This allows you to use them as drop-in replacements for the corre- This allows you to use them as drop-in replacements for the corresponding
sponding Tkinter classes. Tkinter classes.
- Removed bogus references to the crack coder (ImagingCrack). - Removed bogus references to the crack coder (ImagingCrack).

View File

@ -29,3 +29,4 @@ global-exclude .git*
global-exclude *.pyc global-exclude *.pyc
global-exclude *.so global-exclude *.so
prune .ci prune .ci
prune wheels

View File

@ -45,12 +45,12 @@ As of 2019, Pillow development is
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img <a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
alt="AppVeyor CI build status (Windows)" alt="AppVeyor CI build status (Windows)"
src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a> src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a>
<a href="https://github.com/python-pillow/pillow-wheels/actions"><img <a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
alt="GitHub Actions wheels build status (Wheels)" alt="GitHub Actions build status (Wheels)"
src="https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
<a href="https://app.travis-ci.com/github/python-pillow/pillow-wheels"><img <a href="https://app.travis-ci.com/github/python-pillow/Pillow"><img
alt="Travis CI wheels build status (aarch64)" alt="Travis CI wheels build status (aarch64)"
src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels"></a> src="https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels"></a>
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img <a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
alt="Code coverage" alt="Code coverage"
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a> src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
@ -74,9 +74,9 @@ As of 2019, Pillow development is
<a href="https://pypi.org/project/Pillow/"><img <a href="https://pypi.org/project/Pillow/"><img
alt="Number of PyPI downloads" alt="Number of PyPI downloads"
src="https://img.shields.io/pypi/dm/pillow.svg"></a> src="https://img.shields.io/pypi/dm/pillow.svg"></a>
<a href="https://bestpractices.coreinfrastructure.org/projects/6331"><img <a href="https://www.bestpractices.dev/projects/6331"><img
alt="OpenSSF Best Practices" alt="OpenSSF Best Practices"
src="https://bestpractices.coreinfrastructure.org/projects/6331/badge"></a> src="https://www.bestpractices.dev/projects/6331/badge"></a>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `main` branch. * [ ] 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) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
* [ ] Check that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI and GitHub Actions. * [ ] Check that all of the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) jobs by manually triggering them.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Update `CHANGES.rst`. * [ ] Update `CHANGES.rst`.
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
@ -99,17 +99,14 @@ Released as needed privately to individual vendors for critical security-related
## Binary Distributions ## Binary Distributions
### macOS and Linux ### macOS and Linux
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels): * [ ] Download wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
```bash ```bash
git clone https://github.com/python-pillow/pillow-wheels gh run download --dir dist
cd pillow-wheels # select dist-x.y.z
./update-pillow-tag.sh [[release tag]]
```
* [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases)
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli) from the main repo:
```bash
gh release download --dir dist --pattern "*.whl" --repo python-pillow/pillow-wheels
``` ```
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
and copy into `dist`.
### Windows ### Windows
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) * [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)

0
Tests/check_j2k_leaks.py Executable file → Normal file
View File

View File

@ -37,4 +37,4 @@ The Font Software may be sold as part of a larger software package but no copy o
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr. Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr.

View File

@ -91,7 +91,7 @@ def assert_image_equal(a, b, msg=None):
if HAS_UPLOADER: if HAS_UPLOADER:
try: try:
url = test_image_results.upload(a, b) url = test_image_results.upload(a, b)
logger.error(f"Url for test images: {url}") logger.error("URL for test images: %s", url)
except Exception: except Exception:
pass pass
@ -126,7 +126,7 @@ def assert_image_similar(a, b, epsilon, msg=None):
if HAS_UPLOADER: if HAS_UPLOADER:
try: try:
url = test_image_results.upload(a, b) url = test_image_results.upload(a, b)
logger.error(f"Url for test images: {url}") logger.exception("URL for test images: %s", url)
except Exception: except Exception:
pass pass
raise e raise e

View File

@ -22,4 +22,3 @@ and that the name of ICC shall not be used in advertising or publicity
pertaining to distribution of the software without specific, written pertaining to distribution of the software without specific, written
prior permission. ICC makes no representations about the suitability prior permission. ICC makes no representations about the suitability
of this software for any purpose. of this software for any purpose.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 95 KiB

BIN
Tests/images/bc5u.dds Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 180 B

0
Tests/images/negative_size.ppm Executable file → Normal file
View File

BIN
Tests/images/palette.dds Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

BIN
Tests/images/xmp_padded.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

Binary file not shown.

Binary file not shown.

View File

@ -673,10 +673,16 @@ def test_seek_after_close():
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P")) @pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
def test_different_modes_in_later_frames(mode, tmp_path): @pytest.mark.parametrize("default_image", (True, False))
def test_different_modes_in_later_frames(mode, default_image, tmp_path):
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")
im = Image.new("L", (1, 1)) im = Image.new("L", (1, 1))
im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))]) im.save(
test_file,
save_all=True,
default_image=default_image,
append_images=[Image.new(mode, (1, 1))],
)
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
assert reloaded.mode == mode assert reloaded.mode == mode

View File

@ -16,6 +16,7 @@ TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
TEST_FILE_BC5S = "Tests/images/bc5s.dds" TEST_FILE_BC5S = "Tests/images/bc5s.dds"
TEST_FILE_BC5U = "Tests/images/bc5u.dds"
TEST_FILE_BC6H = "Tests/images/bc6h.dds" TEST_FILE_BC6H = "Tests/images/bc6h.dds"
TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds" TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds"
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
@ -81,10 +82,18 @@ def test_sanity_ati1():
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
def test_sanity_ati2(): @pytest.mark.parametrize(
"""Check ATI2 images can be opened""" "image_path",
(
TEST_FILE_ATI2,
# hexeditted to use BC5U FourCC
TEST_FILE_BC5U,
),
)
def test_sanity_ati2_bc5u(image_path):
"""Check ATI2 and BC5U images can be opened"""
with Image.open(TEST_FILE_ATI2) as im: with Image.open(image_path) as im:
im.load() im.load()
assert im.format == "DDS" assert im.format == "DDS"
@ -289,6 +298,11 @@ def test_dxt5_colorblock_alpha_issue_4142():
assert px[2] != 0 assert px[2] != 0
def test_palette():
with Image.open("Tests/images/palette.dds") as im:
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
def test_unimplemented_pixel_format(): def test_unimplemented_pixel_format():
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
with Image.open("Tests/images/unimplemented_pixel_format.dds"): with Image.open("Tests/images/unimplemented_pixel_format.dds"):

View File

@ -8,6 +8,7 @@ from .helper import (
assert_image_similar, assert_image_similar,
assert_image_similar_tofile, assert_image_similar_tofile,
hopper, hopper,
is_win32,
mark_if_feature_version, mark_if_feature_version,
skip_unless_feature, skip_unless_feature,
) )
@ -98,6 +99,20 @@ def test_load():
assert im.load()[0, 0] == (255, 255, 255) assert im.load()[0, 0] == (255, 255, 255)
def test_binary():
if HAS_GHOSTSCRIPT:
assert EpsImagePlugin.gs_binary is not None
else:
assert EpsImagePlugin.gs_binary is False
if not is_win32():
assert EpsImagePlugin.gs_windows_binary is None
elif not HAS_GHOSTSCRIPT:
assert EpsImagePlugin.gs_windows_binary is False
else:
assert EpsImagePlugin.gs_windows_binary is not None
def test_invalid_file(): def test_invalid_file():
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
@ -404,3 +419,18 @@ def test_timeout(test_file):
with pytest.raises(Image.UnidentifiedImageError): with pytest.raises(Image.UnidentifiedImageError):
with Image.open(f): with Image.open(f):
pass pass
def test_bounding_box_in_trailer():
# Check bounding boxes are parsed in the same way
# when specified in the header and the trailer
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
FILE1
) as header_image:
assert trailer_image.size == header_image.size
def test_eof_before_bounding_box():
with pytest.raises(OSError):
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
pass

View File

@ -205,14 +205,14 @@ def test_optimize_full_l():
def test_optimize_if_palette_can_be_reduced_by_half(): def test_optimize_if_palette_can_be_reduced_by_half():
with Image.open("Tests/images/test.colors.gif") as im: im = Image.new("P", (8, 1))
# Reduce dimensions because original is too big for _get_optimize() im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150))
im = im.resize((591, 443)) for i in range(8):
im_rgb = im.convert("RGB") im.putpixel((i, 0), (i + 1, 0, 0))
for optimize, colors in ((False, 256), (True, 8)): for optimize, colors in ((False, 256), (True, 8)):
out = BytesIO() out = BytesIO()
im_rgb.save(out, "GIF", optimize=optimize) im.save(out, "GIF", optimize=optimize)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert len(reloaded.palette.palette) // 3 == colors assert len(reloaded.palette.palette) // 3 == colors
@ -1180,18 +1180,17 @@ def test_palette_save_L(tmp_path):
def test_palette_save_P(tmp_path): def test_palette_save_P(tmp_path):
# Pass in a different palette, then construct what the image would look like. im = Image.new("P", (1, 2))
# Forcing a non-straight grayscale palette. im.putpixel((0, 1), 1)
im = hopper("P")
palette = bytes(255 - i // 3 for i in range(768))
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
im.save(out, palette=palette) im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
im.putpalette(palette) reloaded_rgb = reloaded.convert("RGB")
assert_image_equal(reloaded, im)
assert reloaded_rgb.getpixel((0, 0)) == (1, 2, 3)
assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)
def test_palette_save_duplicate_entries(tmp_path): def test_palette_save_duplicate_entries(tmp_path):

View File

@ -1,5 +1,5 @@
import sys import sys
from io import StringIO from io import BytesIO, StringIO
from PIL import Image, IptcImagePlugin from PIL import Image, IptcImagePlugin
@ -30,6 +30,36 @@ def test_getiptcinfo_jpg_found():
assert iptc[(2, 101)] == b"Hungary" assert iptc[(2, 101)] == b"Hungary"
def test_getiptcinfo_fotostation():
# Arrange
with open(TEST_FILE, "rb") as fp:
data = bytearray(fp.read())
data[86] = 240
f = BytesIO(data)
with Image.open(f) as im:
# Act
iptc = IptcImagePlugin.getiptcinfo(im)
# Assert
for tag in iptc.keys():
if tag[0] == 240:
return
assert False, "FotoStation tag not found"
def test_getiptcinfo_zero_padding():
# Arrange
with Image.open(TEST_FILE) as im:
im.info["photoshop"][0x0404] += b"\x00\x00\x00"
# Act
iptc = IptcImagePlugin.getiptcinfo(im)
# Assert
assert isinstance(iptc, dict)
assert len(iptc) == 3
def test_getiptcinfo_tiff_none(): def test_getiptcinfo_tiff_none():
# Arrange # Arrange
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:

View File

@ -767,6 +767,13 @@ class TestFileJpeg:
# This should return the default # This should return the default
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
def test_dpi_exif_truncated(self):
# Arrange
with Image.open("Tests/images/truncated_exif_dpi.jpg") as im:
# Act / Assert
# This should return the default
assert im.info.get("dpi") == (72, 72)
def test_no_dpi_in_exif(self): def test_no_dpi_in_exif(self):
# Arrange # Arrange
# This is photoshop-200dpi.jpg with resolution removed from EXIF: # This is photoshop-200dpi.jpg with resolution removed from EXIF:
@ -882,7 +889,10 @@ class TestFileJpeg:
def test_getxmp(self): def test_getxmp(self):
with Image.open("Tests/images/xmp_test.jpg") as im: with Image.open("Tests/images/xmp_test.jpg") as im:
if ElementTree is None: if ElementTree is None:
with pytest.warns(UserWarning): with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
xmp = im.getxmp() xmp = im.getxmp()
@ -905,6 +915,28 @@ class TestFileJpeg:
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
assert im.getxmp() == {} assert im.getxmp() == {}
def test_getxmp_no_prefix(self):
with Image.open("Tests/images/xmp_no_prefix.jpg") as im:
if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {}
else:
assert im.getxmp() == {"xmpmeta": {"key": "value"}}
def test_getxmp_padded(self):
with Image.open("Tests/images/xmp_padded.jpg") as im:
if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {}
else:
assert im.getxmp() == {"xmpmeta": None}
@pytest.mark.timeout(timeout=1) @pytest.mark.timeout(timeout=1)
def test_eof(self): def test_eof(self):
# Even though this decoder never says that it is finished # Even though this decoder never says that it is finished

View File

@ -8,7 +8,7 @@ from collections import namedtuple
import pytest import pytest
from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features
from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
from .helper import ( from .helper import (
@ -1035,7 +1035,18 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/g4_orientation_1.tif") as base_im: with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
for i in range(2, 9): for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
assert 274 in im.tag_v2
im.load() im.load()
assert 274 not in im.tag_v2
assert_image_similar(base_im, im, 0.7)
def test_exif_transpose(self):
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
im = ImageOps.exif_transpose(im)
assert_image_similar(base_im, im, 0.7) assert_image_similar(base_im, im, 0.7)

View File

@ -92,11 +92,11 @@ class TestFilePng:
assert im.format == "PNG" assert im.format == "PNG"
assert im.get_format_mimetype() == "image/png" assert im.get_format_mimetype() == "image/png"
for mode in ["1", "L", "P", "RGB", "I", "I;16"]: for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]:
im = hopper(mode) im = hopper(mode)
im.save(test_file) im.save(test_file)
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
if mode == "I;16": if mode in ("I;16", "I;16B"):
reloaded = reloaded.convert(mode) reloaded = reloaded.convert(mode)
assert_image_equal(reloaded, im) assert_image_equal(reloaded, im)
@ -665,7 +665,10 @@ class TestFilePng:
def test_getxmp(self): def test_getxmp(self):
with Image.open("Tests/images/color_snakes.png") as im: with Image.open("Tests/images/color_snakes.png") as im:
if ElementTree is None: if ElementTree is None:
with pytest.warns(UserWarning): with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
xmp = im.getxmp() xmp = im.getxmp()

View File

@ -2,7 +2,7 @@ import pytest
from PIL import Image, QoiImagePlugin from PIL import Image, QoiImagePlugin
from .helper import assert_image_equal_tofile, assert_image_similar_tofile from .helper import assert_image_equal_tofile
def test_sanity(): def test_sanity():
@ -18,7 +18,7 @@ def test_sanity():
assert im.size == (162, 150) assert im.size == (162, 150)
assert im.format == "QOI" assert im.format == "QOI"
assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03) assert_image_equal_tofile(im, "Tests/images/pil123rgba.png")
def test_invalid_file(): def test_invalid_file():

View File

@ -734,7 +734,10 @@ class TestFileTiff:
def test_getxmp(self): def test_getxmp(self):
with Image.open("Tests/images/lab.tif") as im: with Image.open("Tests/images/lab.tif") as im:
if ElementTree is None: if ElementTree is None:
with pytest.warns(UserWarning): with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
xmp = im.getxmp() xmp = im.getxmp()

View File

@ -233,4 +233,15 @@ class TestFileWebp:
im.save(out_webp, save_all=True) im.save(out_webp, save_all=True)
with Image.open(out_webp) as reloaded: with Image.open(out_webp) as reloaded:
reloaded.load()
assert reloaded.info["duration"] == 1000 assert reloaded.info["duration"] == 1000
def test_roundtrip_rgba_palette(self, tmp_path):
temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGBA", (1, 1)).convert("P")
assert im.mode == "P"
assert im.palette.mode == "RGBA"
im.save(temp_file)
with Image.open(temp_file) as im:
assert im.getpixel((0, 0)) == (0, 0, 0, 0)

View File

@ -118,7 +118,10 @@ def test_getxmp():
with Image.open("Tests/images/flower2.webp") as im: with Image.open("Tests/images/flower2.webp") as im:
if ElementTree is None: if ElementTree is None:
with pytest.warns(UserWarning): with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
assert ( assert (

View File

@ -638,8 +638,8 @@ class TestImage:
im.remap_palette(None) im.remap_palette(None)
def test_remap_palette_transparency(self): def test_remap_palette_transparency(self):
im = Image.new("P", (1, 2)) im = Image.new("P", (1, 2), (0, 0, 0))
im.putpixel((0, 1), 1) im.putpixel((0, 1), (255, 0, 0))
im.info["transparency"] = 0 im.info["transparency"] = 0
im_remapped = im.remap_palette([1, 0]) im_remapped = im.remap_palette([1, 0])
@ -906,6 +906,31 @@ class TestImage:
im = Image.new("RGB", size) im = Image.new("RGB", size)
assert im.tobytes() == b"" assert im.tobytes() == b""
def test_has_transparency_data(self):
for mode in ("1", "L", "P", "RGB"):
im = Image.new(mode, (1, 1))
assert not im.has_transparency_data
for mode in ("LA", "La", "PA", "RGBA", "RGBa"):
im = Image.new(mode, (1, 1))
assert im.has_transparency_data
# P mode with "transparency" info
with Image.open("Tests/images/first_frame_transparency.gif") as im:
assert "transparency" in im.info
assert im.has_transparency_data
# RGB mode with "transparency" info
with Image.open("Tests/images/rgb_trns.png") as im:
assert "transparency" in im.info
assert im.has_transparency_data
# P mode with RGBA palette
im = Image.new("RGBA", (1, 1)).convert("P")
assert im.mode == "P"
assert im.palette.mode == "RGBA"
assert im.has_transparency_data
def test_apply_transparency(self): def test_apply_transparency(self):
im = Image.new("P", (1, 1)) im = Image.new("P", (1, 1))
im.putpalette((0, 0, 0, 1, 1, 1)) im.putpalette((0, 0, 0, 1, 1, 1))

View File

@ -130,9 +130,16 @@ class TestImageGetPixel(AccessTest):
bands = Image.getmodebands(mode) bands = Image.getmodebands(mode)
if bands == 1: if bands == 1:
return 1 return 1
if mode in ("BGR;15", "BGR;16"):
# These modes have less than 8 bits per band
# So (1, 2, 3) cannot be roundtripped
return (16, 32, 49)
return tuple(range(1, bands + 1)) return tuple(range(1, bands + 1))
def check(self, mode, expected_color=None): def check(self, mode, expected_color=None):
if self._need_cffi_access and mode.startswith("BGR;"):
pytest.skip("Support not added to deprecated module for BGR;* modes")
if not expected_color: if not expected_color:
expected_color = self.color(mode) expected_color = self.color(mode)
@ -203,6 +210,9 @@ class TestImageGetPixel(AccessTest):
"F", "F",
"P", "P",
"PA", "PA",
"BGR;15",
"BGR;16",
"BGR;24",
"RGB", "RGB",
"RGBA", "RGBA",
"RGBX", "RGBX",
@ -213,6 +223,10 @@ class TestImageGetPixel(AccessTest):
def test_basic(self, mode): def test_basic(self, mode):
self.check(mode) self.check(mode)
def test_list(self):
im = hopper()
assert im.getpixel([0, 0]) == (20, 20, 70)
@pytest.mark.parametrize("mode", ("I;16", "I;16B")) @pytest.mark.parametrize("mode", ("I;16", "I;16B"))
@pytest.mark.parametrize( @pytest.mark.parametrize(
"expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1) "expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)

View File

@ -117,11 +117,11 @@ def test_trns_p(tmp_path):
f = str(tmp_path / "temp.png") f = str(tmp_path / "temp.png")
im_l = im.convert("L") im_l = im.convert("L")
assert im_l.info["transparency"] == 1 # undone assert im_l.info["transparency"] == 0
im_l.save(f) im_l.save(f)
im_rgb = im.convert("RGB") im_rgb = im.convert("RGB")
assert im_rgb.info["transparency"] == (0, 1, 2) # undone assert im_rgb.info["transparency"] == (0, 0, 0)
im_rgb.save(f) im_rgb.save(f)

View File

@ -76,6 +76,15 @@ def test_mode_F():
assert list(im.getdata()) == target assert list(im.getdata()) == target
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
def test_mode_BGR(mode):
data = [(16, 32, 49), (32, 32, 98)]
im = Image.new(mode, (1, 2))
im.putdata(data)
assert list(im.getdata()) == data
def test_array_B(): def test_array_B():
# shouldn't segfault # shouldn't segfault
# see https://github.com/python-pillow/Pillow/issues/1008 # see https://github.com/python-pillow/Pillow/issues/1008

View File

@ -84,3 +84,14 @@ def test_rgba_palette(mode, palette):
im.putpalette(palette, mode) im.putpalette(palette, mode)
assert im.getpalette() == [1, 2, 3] assert im.getpalette() == [1, 2, 3]
assert im.palette.colors == {(1, 2, 3, 4): 0} assert im.palette.colors == {(1, 2, 3, 4): 0}
def test_empty_palette():
im = Image.new("P", (1, 1))
assert im.getpalette() == []
def test_undefined_palette_index():
im = Image.new("P", (1, 1), 3)
im.putpalette((1, 2, 3))
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0)

View File

@ -587,6 +587,18 @@ def test_point(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
def test_point_I16():
# Arrange
im = Image.new("I;16", (1, 1))
draw = ImageDraw.Draw(im)
# Act
draw.point((0, 0), fill=0x1234)
# Assert
assert im.getpixel((0, 0)) == 0x1234
@pytest.mark.parametrize("points", POINTS) @pytest.mark.parametrize("points", POINTS)
def test_polygon(points): def test_polygon(points):
# Arrange # Arrange
@ -733,7 +745,7 @@ def test_rectangle_I16(bbox):
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
draw.rectangle(bbox, fill="black", outline="green") draw.rectangle(bbox, outline=0xFFFF)
# Assert # Assert
assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png") assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")

View File

@ -141,7 +141,9 @@ def test_I16(font):
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
txt = "Hello World!" txt = "Hello World!"
draw.text((10, 10), txt, font=font) draw.text((10, 10), txt, fill=0xFFFE, font=font)
assert im.getpixel((12, 14)) == 0xFFFE
target = "Tests/images/transparent_background_text_L.png" target = "Tests/images/transparent_background_text_L.png"
assert_image_similar_tofile(im.convert("L"), target, 0.01) assert_image_similar_tofile(im.convert("L"), target, 0.01)

View File

@ -39,6 +39,9 @@ def test_sanity():
ImageOps.contain(hopper("L"), (128, 128)) ImageOps.contain(hopper("L"), (128, 128))
ImageOps.contain(hopper("RGB"), (128, 128)) ImageOps.contain(hopper("RGB"), (128, 128))
ImageOps.cover(hopper("L"), (128, 128))
ImageOps.cover(hopper("RGB"), (128, 128))
ImageOps.crop(hopper("L"), 1) ImageOps.crop(hopper("L"), 1)
ImageOps.crop(hopper("RGB"), 1) ImageOps.crop(hopper("RGB"), 1)
@ -119,6 +122,20 @@ def test_contain_round():
assert new_im.height == 5 assert new_im.height == 5
@pytest.mark.parametrize(
"image_name, expected_size",
(
("colr_bungee.png", (1024, 256)), # landscape
("imagedraw_stroke_multiline.png", (256, 640)), # portrait
("hopper.png", (256, 256)), # square
),
)
def test_cover(image_name, expected_size):
with Image.open("Tests/images/" + image_name) as im:
new_im = ImageOps.cover(im, (256, 256))
assert new_im.size == expected_size
def test_pad(): def test_pad():
# Same ratio # Same ratio
im = hopper() im = hopper()

View File

@ -340,6 +340,17 @@ class TestLibUnpack:
self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0)) self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0))
self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5)) self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5))
self.assert_unpack(
"RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233)
)
def test_BGR(self):
self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8))
self.assert_unpack(
"BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0)
)
self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
def test_RGBA(self): def test_RGBA(self):
self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))
self.assert_unpack( self.assert_unpack(

0
_custom_build/backend.py Executable file → Normal file
View File

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install libimagequant # install libimagequant
archive=libimagequant-4.2.0 archive=libimagequant-4.2.1
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -11,4 +11,3 @@ pushd $archive
meson build --prefix=/usr && sudo ninja -C build install meson build --prefix=/usr && sudo ninja -C build install
popd popd

View File

@ -15,4 +15,3 @@ make && sudo make install
cd .. cd ..
popd popd

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install webp # install webp
archive=libwebp-1.3.1 archive=libwebp-1.3.2
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -2,4 +2,3 @@
pkg install -y python ndk-sysroot clang make \ pkg install -y python ndk-sysroot clang make \
libjpeg-turbo libjpeg-turbo

View File

@ -12,7 +12,7 @@ The fork author's goal is to foster and support active development of PIL throug
.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow .. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
.. _Travis CI: https://app.travis-ci.com/github/python-pillow/pillow-wheels .. _Travis CI: https://app.travis-ci.com/github/python-pillow/Pillow
.. _GitHub: https://github.com/python-pillow/Pillow .. _GitHub: https://github.com/python-pillow/Pillow
.. _Python Package Index: https://pypi.org/project/Pillow/ .. _Python Package Index: https://pypi.org/project/Pillow/

View File

@ -318,7 +318,7 @@ def setup(app):
linkcheck_allowed_redirects = { linkcheck_allowed_redirects = {
r"https://bestpractices.coreinfrastructure.org/projects/6331": r"https://bestpractices.coreinfrastructure.org/en/.*", # noqa: E501 r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*", # noqa: E501
r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501 r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501
r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501 r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501
r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501 r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -63,8 +63,35 @@ DDS
^^^ ^^^
DDS is a popular container texture format used in video games and natively supported DDS is a popular container texture format used in video games and natively supported
by DirectX. Uncompressed RGB and RGBA can be read, and (since 8.3.0) written. DXT1, by DirectX.
DXT3 (since 3.4.0) and DXT5 pixel formats can be read, only in ``RGBA`` mode.
DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode.
.. versionadded:: 3.4.0
DXT3 images can be read in ``RGB`` mode and DX10 images can be read in
``RGB`` and ``RGBA`` mode.
.. versionadded:: 6.0.0
Uncompressed ``RGBA`` images can be read.
.. versionadded:: 8.3.0
BC5S images can be opened in ``RGB`` mode, and uncompressed ``RGB`` images
can be read. Uncompressed data can also be saved to image files.
.. versionadded:: 9.3.0
ATI1 images can be opened in ``L`` mode and ATI2 images can be opened in
``RGB`` mode.
.. versionadded:: 9.4.0
Uncompressed ``L`` ("luminance") and ``LA`` images can be opened and saved.
.. versionadded:: 10.1.0
BC5U can be read in ``RGB`` mode, and 8-bit color indexed images can be read
in ``P`` mode.
DIB DIB
^^^ ^^^
@ -88,8 +115,13 @@ in ``L``, ``RGB`` and ``CMYK`` modes.
Loading Loading
~~~~~~~ ~~~~~~~
To use Ghostscript, Pillow searches for the "gs" executable. On Windows, it
also searches for "gswin32c" and "gswin64c". To customise this behaviour,
``EpsImagePlugin.gs_binary = "gswin64"`` will set the name of the executable to
use. ``EpsImagePlugin.gs_binary = False`` will prevent Ghostscript use.
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load` If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
method with the following parameters to affect how Ghostscript renders the EPS method with the following parameters to affect how Ghostscript renders the EPS.
**scale** **scale**
Affects the scale of the resultant rasterized image. If the EPS suggests Affects the scale of the resultant rasterized image. If the EPS suggests

View File

@ -268,6 +268,37 @@ true, to provide for the same changes to the image's size.
A more general form of image transformations can be carried out via the A more general form of image transformations can be carried out via the
:py:meth:`~PIL.Image.Image.transform` method. :py:meth:`~PIL.Image.Image.transform` method.
Relative resizing
^^^^^^^^^^^^^^^^^
Instead of calculating the size of the new image when resizing, you can also
choose to resize relative to a given size.
::
from PIL import Image, ImageOps
size = (100, 150)
with Image.open("Tests/images/hopper.png") as im:
ImageOps.contain(im, size).save("imageops_contain.png")
ImageOps.cover(im, size).save("imageops_cover.png")
ImageOps.fit(im, size).save("imageops_fit.png")
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
# thumbnail() can also be used,
# but will modify the image object in place
im.thumbnail(size)
im.save("imageops_thumbnail.png")
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
+================+===========================================+============================================+==========================================+========================================+========================================+
|Given size | ``(150, 100)`` | ``(150, 100)`` | ``(150, 150)`` | ``(150, 100)`` | ``(150, 100)`` |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``150×100`` | ``150×100`` |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
.. _color-transforms: .. _color-transforms:
Color transforms Color transforms

View File

@ -37,12 +37,12 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://ci.appveyor.com/project/python-pillow/Pillow :target: https://ci.appveyor.com/project/python-pillow/Pillow
:alt: AppVeyor CI build status (Windows) :alt: AppVeyor CI build status (Windows)
.. image:: https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg .. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
:target: https://github.com/python-pillow/pillow-wheels/actions :target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
:alt: GitHub Actions wheels build status (Wheels) :alt: GitHub Actions build status (Wheels)
.. image:: https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels .. image:: https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels
:target: https://app.travis-ci.com/github/python-pillow/pillow-wheels :target: https://app.travis-ci.com/github/python-pillow/Pillow
:alt: Travis CI wheels build status (aarch64) :alt: Travis CI wheels build status (aarch64)
.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg .. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg
@ -69,8 +69,8 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://pypi.org/project/Pillow/ :target: https://pypi.org/project/Pillow/
:alt: Number of PyPI downloads :alt: Number of PyPI downloads
.. image:: https://bestpractices.coreinfrastructure.org/projects/6331/badge .. image:: https://www.bestpractices.dev/projects/6331/badge
:target: https://bestpractices.coreinfrastructure.org/projects/6331 :target: https://www.bestpractices.dev/projects/6331
:alt: OpenSSF Best Practices :alt: OpenSSF Best Practices
.. image:: https://badges.gitter.im/python-pillow/Pillow.svg .. image:: https://badges.gitter.im/python-pillow/Pillow.svg

View File

@ -82,6 +82,8 @@ Install Pillow with :command:`pip`::
.. tab:: Windows .. tab:: Windows
.. warning:: Pillow > 9.5.0 no longer includes 32-bit wheels.
We provide Pillow binaries for Windows compiled for the matrix of We provide Pillow binaries for Windows compiled for the matrix of
supported Pythons in 64-bit versions in the wheel format. These binaries include supported Pythons in 64-bit versions in the wheel format. These binaries include
support for all optional libraries except libimagequant and libxcb. Raqm support support for all optional libraries except libimagequant and libxcb. Raqm support
@ -154,7 +156,7 @@ Many of Pillow's features require external libraries:
* **libtiff** provides compressed TIFF functionality * **libtiff** provides compressed TIFF functionality
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.5.1** * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0**
* **libfreetype** provides type related services * **libfreetype** provides type related services
@ -180,7 +182,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization * **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.2** * Pillow has been tested with libimagequant **2.6-4.2.1**
* Libimagequant is licensed GPLv3, which is more restrictive than * Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled. with libimagequant support enabled.
@ -498,11 +500,13 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested | | Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors | | | | versions | | Pillow version | | processors |
+==================================+===========================+==================+==============+ +==================================+===========================+==================+==============+
| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.0 |arm | | macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
| +---------------------------+------------------+ |
| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.5.0 | |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | | macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
| +---------------------------+------------------+ |
| | 3.7 | 9.5.0 | |
+----------------------------------+---------------------------+------------------+--------------+
| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
| +---------------------------+------------------+--------------+ | +---------------------------+------------------+--------------+

View File

@ -5,4 +5,4 @@ Pillow 9.3 - 9.5,,Yes,Yes,Yes,Yes,Yes,,
Pillow 9.0 - 9.2,,,Yes,Yes,Yes,Yes,, Pillow 9.0 - 9.2,,,Yes,Yes,Yes,Yes,,
Pillow 8.3.2 - 8.4,,,Yes,Yes,Yes,Yes,Yes, Pillow 8.3.2 - 8.4,,,Yes,Yes,Yes,Yes,Yes,
Pillow 8.0 - 8.3.1,,,,Yes,Yes,Yes,Yes, Pillow 8.0 - 8.3.1,,,,Yes,Yes,Yes,Yes,
Pillow 7.0 - 7.2,,,,,Yes,Yes,Yes,Yes Pillow 7.0 - 7.2,,,,,Yes,Yes,Yes,Yes

1 Python 3.12 3.11 3.10 3.9 3.8 3.7 3.6 3.5
5 Pillow 9.0 - 9.2 Yes Yes Yes Yes
6 Pillow 8.3.2 - 8.4 Yes Yes Yes Yes Yes
7 Pillow 8.0 - 8.3.1 Yes Yes Yes Yes
8 Pillow 7.0 - 7.2 Yes Yes Yes Yes

View File

@ -5,4 +5,4 @@ Pillow 5.2 - 5.4,,Yes,Yes,Yes,Yes,,,Yes,,,
Pillow 5.0 - 5.1,,,Yes,Yes,Yes,,,Yes,,, Pillow 5.0 - 5.1,,,Yes,Yes,Yes,,,Yes,,,
Pillow 4,,,Yes,Yes,Yes,Yes,,Yes,,, Pillow 4,,,Yes,Yes,Yes,Yes,,Yes,,,
Pillow 2 - 3,,,,Yes,Yes,Yes,Yes,Yes,Yes,, Pillow 2 - 3,,,,Yes,Yes,Yes,Yes,Yes,Yes,,
Pillow < 2,,,,,,,,Yes,Yes,Yes,Yes Pillow < 2,,,,,,,,Yes,Yes,Yes,Yes

1 Python 3.8 3.7 3.6 3.5 3.4 3.3 3.2 2.7 2.6 2.5 2.4
5 Pillow 5.0 - 5.1 Yes Yes Yes Yes
6 Pillow 4 Yes Yes Yes Yes Yes
7 Pillow 2 - 3 Yes Yes Yes Yes Yes Yes
8 Pillow < 2 Yes Yes Yes Yes

View File

@ -93,10 +93,14 @@ Generating images
Registering plugins Registering plugins
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
.. autofunction:: preinit
.. autofunction:: init
.. note:: .. note::
These functions are for use by plugin authors. Application authors can These functions are for use by plugin authors. They are called when a
ignore them. plugin is loaded as part of :py:meth:`~preinit()` or :py:meth:`~init()`.
Application authors can ignore them.
.. autofunction:: register_open .. autofunction:: register_open
.. autofunction:: register_mime .. autofunction:: register_mime
@ -347,6 +351,8 @@ Instances of the :py:class:`Image` class have the following attributes:
.. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell` .. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell`
.. autoattribute:: PIL.Image.Image.has_transparency_data
Classes Classes
------- -------

View File

@ -12,14 +12,11 @@ only work on L and RGB images.
.. autofunction:: autocontrast .. autofunction:: autocontrast
.. autofunction:: colorize .. autofunction:: colorize
.. autofunction:: contain
.. autofunction:: pad
.. autofunction:: crop .. autofunction:: crop
.. autofunction:: scale .. autofunction:: scale
.. autofunction:: deform .. autofunction:: deform
.. autofunction:: equalize .. autofunction:: equalize
.. autofunction:: expand .. autofunction:: expand
.. autofunction:: fit
.. autofunction:: flip .. autofunction:: flip
.. autofunction:: grayscale .. autofunction:: grayscale
.. autofunction:: invert .. autofunction:: invert
@ -27,3 +24,38 @@ only work on L and RGB images.
.. autofunction:: posterize .. autofunction:: posterize
.. autofunction:: solarize .. autofunction:: solarize
.. autofunction:: exif_transpose .. autofunction:: exif_transpose
.. _relative-resize:
Resize relative to a given size
-------------------------------
::
from PIL import Image, ImageOps
size = (100, 150)
with Image.open("Tests/images/hopper.png") as im:
ImageOps.contain(im, size).save("imageops_contain.png")
ImageOps.cover(im, size).save("imageops_cover.png")
ImageOps.fit(im, size).save("imageops_fit.png")
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
# thumbnail() can also be used,
# but will modify the image object in place
im.thumbnail(size)
im.save("imageops_thumbnail.png")
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
+================+===========================================+============================================+==========================================+========================================+========================================+
|Given size | ``(150, 100)`` | ``(150, 100)`` | ``(150, 150)`` | ``(150, 100)`` | ``(150, 100)`` |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``150×100`` | ``150×100`` |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
.. autofunction:: contain
.. autofunction:: cover
.. autofunction:: fit
.. autofunction:: pad

View File

@ -0,0 +1,14 @@
10.0.1
------
Security
========
This release addresses :cve:`2023-4863`, by providing an updated install script and
updated wheels to include libwebp 1.3.2, preventing a potential heap buffer overflow
in WebP.
Updated tests to pass with latest zlib version
==============================================
The release of zlib 1.3 caused one of the tests in the Pillow test suite to fail.

View File

@ -13,42 +13,73 @@ not about removing existing functionality, but instead about raising an
explicit error to prevent later consequences. The ``convert`` method is the explicit error to prevent later consequences. The ``convert`` method is the
correct way to change an image's mode. correct way to change an image's mode.
Deprecations
============
TODO
^^^^
TODO
API Changes API Changes
=========== ===========
TODO Accept a list in getpixel()
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO :py:meth:`~PIL.Image.Image.getpixel` now accepts a list of coordinates, as well
as a tuple. ::
from PIL import Image
im = Image.new("RGB", (1, 1))
im.getpixel((0, 0))
im.getpixel([0, 0])
BoxBlur and GaussianBlur allow for different x and y radii
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:py:class:`~PIL.ImageFilter.BoxBlur` and
:py:class:`~PIL.ImageFilter.GaussianBlur` now allow a sequence of x and y radii
to be specified, rather than a single number for both dimensions. ::
from PIL import ImageFilter
ImageFilter.BoxBlur((2, 5))
ImageFilter.GaussianBlur((2, 5))
API Additions API Additions
============= =============
TODO ImageOps.cover
^^^^ ^^^^^^^^^^^^^^
TODO Returns a resized version of the image, so that the requested size is covered,
while maintaining the original aspect ratio.
Security See :ref:`relative-resize` for a comparison between this and similar ``ImageOps``
======== methods.
TODO EpsImagePlugin.gs_binary
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
TODO ``EpsImagePlugin.gs_windows_binary`` stores the name of the Ghostscript
executable on Windows. ``EpsImagePlugin.gs_binary`` has now been added for all
platforms, and can be used to customise the name of the executable, or disable
use entirely through ``EpsImagePlugin.gs_binary = False``.
has_transparency_data
^^^^^^^^^^^^^^^^^^^^^
Images now have :py:attr:`~PIL.Image.Image.has_transparency_data` to indicate
whether the image has transparency data, whether in the form of an alpha
channel, a palette with an alpha channel, or a "transparency" key in the
:py:attr:`~PIL.Image.Image.info` dictionary.
Even if this attribute is true, the image might still appear solid, if all of
the values shown within are opaque.
Other Changes Other Changes
============= =============
TODO Added support for DDS BC5U and 8-bit color indexed images
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO Support has been added to read BC5U DDS files as RGB images, and
PALETTEINDEXED8 DDS files as P mode images.
Support reading signed 8-bit YCbCr TIFF images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TIFF images with unsigned integer data, 8 bits per sample and a photometric
interpretation of YCbCr can now be read.

View File

@ -49,4 +49,3 @@ The external dependencies on libjpeg and zlib are now required by default.
If the headers or libraries are not found, then installation will abort If the headers or libraries are not found, then installation will abort
with an error. This behaviour can be disabled with the ``--disable-libjpeg`` with an error. This behaviour can be disabled with the ``--disable-libjpeg``
and ``--disable-zlib`` flags. and ``--disable-zlib`` flags.

View File

@ -34,7 +34,3 @@ image size can lead to a smaller allocation than expected, leading to
arbitrary writes. arbitrary writes.
This issue was found by Cris Neckar at Divergent Security. This issue was found by Cris Neckar at Divergent Security.

View File

@ -20,5 +20,3 @@ CPython 3.6.1 to not work on installations of C-Python 3.6.0. This fix
undefines PySlice_GetIndicesEx if it exists to restore compatibility undefines PySlice_GetIndicesEx if it exists to restore compatibility
with both 3.6.0 and 3.6.1. See https://bugs.python.org/issue29943 for with both 3.6.0 and 3.6.1. See https://bugs.python.org/issue29943 for
more details. more details.

View File

@ -8,4 +8,3 @@ Fixed Windows PyPy Build
A change in the 4.2.0 cycle broke the Windows PyPy build. This has A change in the 4.2.0 cycle broke the Windows PyPy build. This has
been fixed, and PyPy is now part of the Windows CI matrix. been fixed, and PyPy is now part of the Windows CI matrix.

View File

@ -175,6 +175,3 @@ Dark theme for docs
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query. The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query.

View File

@ -15,6 +15,7 @@ expected to be backported to earlier versions.
:maxdepth: 2 :maxdepth: 2
10.1.0 10.1.0
10.0.1
10.0.0 10.0.0
9.5.0 9.5.0
9.4.0 9.4.0

View File

@ -68,11 +68,11 @@ def bdf_char(f):
# followed by the width in x (BBw), height in y (BBh), # followed by the width in x (BBw), height in y (BBh),
# and x and y displacement (BBxoff0, BByoff0) # and x and y displacement (BBxoff0, BByoff0)
# of the lower left corner from the origin of the character. # of the lower left corner from the origin of the character.
width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split())
# The word DWIDTH # The word DWIDTH
# followed by the width in x and y of the character in device pixels. # followed by the width in x and y of the character in device pixels.
dwx, dwy = [int(p) for p in props["DWIDTH"].split()] dwx, dwy = (int(p) for p in props["DWIDTH"].split())
bbox = ( bbox = (
(dwx, dwy), (dwx, dwy),

View File

@ -419,9 +419,11 @@ class BLPEncoder(ImageFile.PyEncoder):
def _write_palette(self): def _write_palette(self):
data = b"" data = b""
palette = self.im.getpalette("RGBA", "RGBA") palette = self.im.getpalette("RGBA", "RGBA")
for i in range(256): for i in range(len(palette) // 4):
r, g, b, a = palette[i * 4 : (i + 1) * 4] r, g, b, a = palette[i * 4 : (i + 1) * 4]
data += struct.pack("<4B", b, g, r, a) data += struct.pack("<4B", b, g, r, a)
while len(data) < 256 * 4:
data += b"\x00" * 4
return data return data
def encode(self, bufsize): def encode(self, bufsize):
@ -442,7 +444,7 @@ class BLPEncoder(ImageFile.PyEncoder):
return len(data), 0, data return len(data), 0, data
def _save(im, fp, filename, save_all=False): def _save(im, fp, filename):
if im.mode != "P": if im.mode != "P":
msg = "Unsupported BLP image mode" msg = "Unsupported BLP image mode"
raise ValueError(msg) raise ValueError(msg)

View File

@ -13,7 +13,7 @@ Full text of the CC0 license:
import struct import struct
from io import BytesIO from io import BytesIO
from . import Image, ImageFile from . import Image, ImageFile, ImagePalette
from ._binary import o32le as o32 from ._binary import o32le as o32
# Magic ("DDS ") # Magic ("DDS ")
@ -157,6 +157,10 @@ class DdsImageFile(ImageFile.ImageFile):
rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF] rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF]
self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))] self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))]
elif pfflags & DDPF_PALETTEINDEXED8:
self._mode = "P"
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
self.tile = [("raw", (0, 0) + self.size, 0, "L")]
else: else:
data_start = header_size + 4 data_start = header_size + 4
n = 0 n = 0
@ -173,7 +177,7 @@ class DdsImageFile(ImageFile.ImageFile):
self.pixel_format = "BC4" self.pixel_format = "BC4"
n = 4 n = 4
self._mode = "L" self._mode = "L"
elif fourcc == b"ATI2": elif fourcc in (b"ATI2", b"BC5U"):
self.pixel_format = "BC5" self.pixel_format = "BC5"
n = 5 n = 5
self._mode = "RGB" self._mode = "RGB"

View File

@ -37,33 +37,39 @@ from ._deprecate import deprecate
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
gs_binary = None
gs_windows_binary = None gs_windows_binary = None
if sys.platform.startswith("win"):
import shutil
for binary in ("gswin32c", "gswin64c", "gs"):
if shutil.which(binary) is not None:
gs_windows_binary = binary
break
else:
gs_windows_binary = False
def has_ghostscript(): def has_ghostscript():
if gs_windows_binary: global gs_binary, gs_windows_binary
return True if gs_binary is None:
if not sys.platform.startswith("win"): if sys.platform.startswith("win"):
try: if gs_windows_binary is None:
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL) import shutil
return True
except OSError: for binary in ("gswin32c", "gswin64c", "gs"):
# No Ghostscript if shutil.which(binary) is not None:
pass gs_windows_binary = binary
return False break
else:
gs_windows_binary = False
gs_binary = gs_windows_binary
else:
try:
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
gs_binary = "gs"
except OSError:
gs_binary = False
return gs_binary is not False
def Ghostscript(tile, size, fp, scale=1, transparency=False): def Ghostscript(tile, size, fp, scale=1, transparency=False):
"""Render an image using Ghostscript""" """Render an image using Ghostscript"""
global gs_binary
if not has_ghostscript():
msg = "Unable to locate Ghostscript on paths"
raise OSError(msg)
# Unpack decoder tile # Unpack decoder tile
decoder, tile, offset, data = tile[0] decoder, tile, offset, data = tile[0]
@ -113,7 +119,7 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
# Build Ghostscript command # Build Ghostscript command
command = [ command = [
"gs", gs_binary,
"-q", # quiet mode "-q", # quiet mode
"-g%dx%d" % size, # set output geometry (pixels) "-g%dx%d" % size, # set output geometry (pixels)
"-r%fx%f" % res, # set input DPI (dots per inch) "-r%fx%f" % res, # set input DPI (dots per inch)
@ -132,19 +138,6 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
"showpage", "showpage",
] ]
if gs_windows_binary is not None:
if not gs_windows_binary:
try:
os.unlink(outfile)
if infile_temp:
os.unlink(infile_temp)
except OSError:
pass
msg = "Unable to locate Ghostscript on paths"
raise OSError(msg)
command[0] = gs_windows_binary
# push data through Ghostscript # push data through Ghostscript
try: try:
startupinfo = None startupinfo = None
@ -233,7 +226,9 @@ class EpsImageFile(ImageFile.ImageFile):
byte_arr = bytearray(255) byte_arr = bytearray(255)
bytes_mv = memoryview(byte_arr) bytes_mv = memoryview(byte_arr)
bytes_read = 0 bytes_read = 0
reading_comments = True reading_header_comments = True
reading_trailer_comments = False
trailer_reached = False
def check_required_header_comments(): def check_required_header_comments():
if "PS-Adobe" not in self.info: if "PS-Adobe" not in self.info:
@ -243,6 +238,36 @@ class EpsImageFile(ImageFile.ImageFile):
msg = 'EPS header missing "%%BoundingBox" comment' msg = 'EPS header missing "%%BoundingBox" comment'
raise SyntaxError(msg) raise SyntaxError(msg)
def _read_comment(s):
nonlocal reading_trailer_comments
try:
m = split.match(s)
except re.error as e:
msg = "not an EPS file"
raise SyntaxError(msg) from e
if m:
k, v = m.group(1, 2)
self.info[k] = v
if k == "BoundingBox":
if v == "(atend)":
reading_trailer_comments = True
elif not self._size or (
trailer_reached and reading_trailer_comments
):
try:
# Note: The DSC spec says that BoundingBox
# fields should be integers, but some drivers
# put floating point values there anyway.
box = [int(float(i)) for i in v.split()]
self._size = box[2] - box[0], box[3] - box[1]
self.tile = [
("eps", (0, 0) + self.size, offset, (length, box))
]
except Exception:
pass
return True
while True: while True:
byte = self.fp.read(1) byte = self.fp.read(1)
if byte == b"": if byte == b"":
@ -265,9 +290,9 @@ class EpsImageFile(ImageFile.ImageFile):
msg = "not an EPS file" msg = "not an EPS file"
raise SyntaxError(msg) raise SyntaxError(msg)
else: else:
if reading_comments: if reading_header_comments:
check_required_header_comments() check_required_header_comments()
reading_comments = False reading_header_comments = False
# reset bytes_read so we can keep reading # reset bytes_read so we can keep reading
# data until the end of the line # data until the end of the line
bytes_read = 0 bytes_read = 0
@ -275,7 +300,7 @@ class EpsImageFile(ImageFile.ImageFile):
bytes_read += 1 bytes_read += 1
continue continue
if reading_comments: if reading_header_comments:
# Load EPS header # Load EPS header
# if this line doesn't start with a "%", # if this line doesn't start with a "%",
@ -283,33 +308,11 @@ class EpsImageFile(ImageFile.ImageFile):
# then we've reached the end of the header/comments # then we've reached the end of the header/comments
if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
check_required_header_comments() check_required_header_comments()
reading_comments = False reading_header_comments = False
continue continue
s = str(bytes_mv[:bytes_read], "latin-1") s = str(bytes_mv[:bytes_read], "latin-1")
if not _read_comment(s):
try:
m = split.match(s)
except re.error as e:
msg = "not an EPS file"
raise SyntaxError(msg) from e
if m:
k, v = m.group(1, 2)
self.info[k] = v
if k == "BoundingBox":
try:
# Note: The DSC spec says that BoundingBox
# fields should be integers, but some drivers
# put floating point values there anyway.
box = [int(float(i)) for i in v.split()]
self._size = box[2] - box[0], box[3] - box[1]
self.tile = [
("eps", (0, 0) + self.size, offset, (length, box))
]
except Exception:
pass
else:
m = field.match(s) m = field.match(s)
if m: if m:
k = m.group(1) k = m.group(1)
@ -339,9 +342,9 @@ class EpsImageFile(ImageFile.ImageFile):
# data start identifier (the image data follows after a single line # data start identifier (the image data follows after a single line
# consisting only of this quoted value) # consisting only of this quoted value)
image_data_values = byte_arr[11:bytes_read].split(None, 7) image_data_values = byte_arr[11:bytes_read].split(None, 7)
columns, rows, bit_depth, mode_id = [ columns, rows, bit_depth, mode_id = (
int(value) for value in image_data_values[:4] int(value) for value in image_data_values[:4]
] )
if bit_depth == 1: if bit_depth == 1:
self._mode = "1" self._mode = "1"
@ -355,7 +358,18 @@ class EpsImageFile(ImageFile.ImageFile):
self._size = columns, rows self._size = columns, rows
return return
elif trailer_reached and reading_trailer_comments:
# Load EPS trailer
# if this line starts with "%%EOF",
# then we've reached the end of the file
if bytes_mv[:5] == b"%%EOF":
break
s = str(bytes_mv[:bytes_read], "latin-1")
_read_comment(s)
elif bytes_mv[:9] == b"%%Trailer":
trailer_reached = True
bytes_read = 0 bytes_read = 0
check_required_header_comments() check_required_header_comments()

View File

@ -298,7 +298,11 @@ _initialized = 0
def preinit(): def preinit():
"""Explicitly load standard file format drivers.""" """
Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers.
It is called when opening or saving images.
"""
global _initialized global _initialized
if _initialized >= 1: if _initialized >= 1:
@ -334,11 +338,6 @@ def preinit():
assert PngImagePlugin assert PngImagePlugin
except ImportError: except ImportError:
pass pass
# try:
# import TiffImagePlugin
# assert TiffImagePlugin
# except ImportError:
# pass
_initialized = 1 _initialized = 1
@ -347,6 +346,9 @@ def init():
""" """
Explicitly initializes the Python Imaging Library. This function Explicitly initializes the Python Imaging Library. This function
loads all available file format drivers. loads all available file format drivers.
It is called when opening or saving images if :py:meth:`~preinit()` is
insufficient, and by :py:meth:`~PIL.features.pilinfo`.
""" """
global _initialized global _initialized
@ -913,7 +915,7 @@ class Image:
self.load() self.load()
has_transparency = self.info.get("transparency") is not None has_transparency = "transparency" in self.info
if not mode and self.mode == "P": if not mode and self.mode == "P":
# determine default mode # determine default mode
if self.palette: if self.palette:
@ -931,9 +933,9 @@ class Image:
msg = "illegal conversion" msg = "illegal conversion"
raise ValueError(msg) raise ValueError(msg)
im = self.im.convert_matrix(mode, matrix) im = self.im.convert_matrix(mode, matrix)
new = self._new(im) new_im = self._new(im)
if has_transparency and self.im.bands == 3: if has_transparency and self.im.bands == 3:
transparency = new.info["transparency"] transparency = new_im.info["transparency"]
def convert_transparency(m, v): def convert_transparency(m, v):
v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5
@ -946,8 +948,8 @@ class Image:
convert_transparency(matrix[i * 4 : i * 4 + 4], transparency) convert_transparency(matrix[i * 4 : i * 4 + 4], transparency)
for i in range(0, len(transparency)) for i in range(0, len(transparency))
) )
new.info["transparency"] = transparency new_im.info["transparency"] = transparency
return new return new_im
if mode == "P" and self.mode == "RGBA": if mode == "P" and self.mode == "RGBA":
return self.quantize(colors) return self.quantize(colors)
@ -978,7 +980,7 @@ class Image:
else: else:
# get the new transparency color. # get the new transparency color.
# use existing conversions # use existing conversions
trns_im = Image()._new(core.new(self.mode, (1, 1))) trns_im = new(self.mode, (1, 1))
if self.mode == "P": if self.mode == "P":
trns_im.putpalette(self.palette) trns_im.putpalette(self.palette)
if isinstance(t, tuple): if isinstance(t, tuple):
@ -1019,23 +1021,25 @@ class Image:
if mode == "P" and palette == Palette.ADAPTIVE: if mode == "P" and palette == Palette.ADAPTIVE:
im = self.im.quantize(colors) im = self.im.quantize(colors)
new = self._new(im) new_im = self._new(im)
from . import ImagePalette from . import ImagePalette
new.palette = ImagePalette.ImagePalette("RGB", new.im.getpalette("RGB")) new_im.palette = ImagePalette.ImagePalette(
"RGB", new_im.im.getpalette("RGB")
)
if delete_trns: if delete_trns:
# This could possibly happen if we requantize to fewer colors. # This could possibly happen if we requantize to fewer colors.
# The transparency would be totally off in that case. # The transparency would be totally off in that case.
del new.info["transparency"] del new_im.info["transparency"]
if trns is not None: if trns is not None:
try: try:
new.info["transparency"] = new.palette.getcolor(trns, new) new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
except Exception: except Exception:
# if we can't make a transparent color, don't leave the old # if we can't make a transparent color, don't leave the old
# transparency hanging around to mess us up. # transparency hanging around to mess us up.
del new.info["transparency"] del new_im.info["transparency"]
warnings.warn("Couldn't allocate palette entry for transparency") warnings.warn("Couldn't allocate palette entry for transparency")
return new return new_im
if "LAB" in (self.mode, mode): if "LAB" in (self.mode, mode):
other_mode = mode if self.mode == "LAB" else self.mode other_mode = mode if self.mode == "LAB" else self.mode
@ -1072,7 +1076,7 @@ class Image:
if mode == "P" and palette != Palette.ADAPTIVE: if mode == "P" and palette != Palette.ADAPTIVE:
from . import ImagePalette from . import ImagePalette
new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB"))
if delete_trns: if delete_trns:
# crash fail if we leave a bytes transparency in an rgb/l mode. # crash fail if we leave a bytes transparency in an rgb/l mode.
del new_im.info["transparency"] del new_im.info["transparency"]
@ -1383,7 +1387,7 @@ class Image:
def _getxmp(self, xmp_tags): def _getxmp(self, xmp_tags):
def get_name(tag): def get_name(tag):
return tag.split("}")[1] return re.sub("^{[^}]+}", "", tag)
def get_value(element): def get_value(element):
value = {get_name(k): v for k, v in element.attrib.items()} value = {get_name(k): v for k, v in element.attrib.items()}
@ -1529,6 +1533,24 @@ class Image:
rawmode = mode rawmode = mode
return list(self.im.getpalette(mode, rawmode)) return list(self.im.getpalette(mode, rawmode))
@property
def has_transparency_data(self) -> bool:
"""
Determine if an image has transparency data, whether in the form of an
alpha channel, a palette with an alpha channel, or a "transparency" key
in the info dictionary.
Note the image might still appear solid, if all of the values shown
within are opaque.
:returns: A boolean.
"""
return (
self.mode in ("LA", "La", "PA", "RGBA", "RGBa")
or (self.mode == "P" and self.palette.mode.endswith("A"))
or "transparency" in self.info
)
def apply_transparency(self): def apply_transparency(self):
""" """
If a P mode image has a "transparency" key in the info dictionary, If a P mode image has a "transparency" key in the info dictionary,
@ -1565,7 +1587,7 @@ class Image:
self.load() self.load()
if self.pyaccess: if self.pyaccess:
return self.pyaccess.getpixel(xy) return self.pyaccess.getpixel(xy)
return self.im.getpixel(xy) return self.im.getpixel(tuple(xy))
def getprojection(self): def getprojection(self):
""" """
@ -3000,7 +3022,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args):
if args == (): if args == ():
args = mode, 0, 1 args = mode, 0, 1
if args[0] in _MAPMODES: if args[0] in _MAPMODES:
im = new(mode, (1, 1)) im = new(mode, (0, 0))
im = im._new(core.map_buffer(data, size, decoder_name, 0, args)) im = im._new(core.map_buffer(data, size, decoder_name, 0, args))
if mode == "P": if mode == "P":
from . import ImagePalette from . import ImagePalette
@ -3407,8 +3429,12 @@ def register_open(id, factory, accept=None):
def register_mime(id, mimetype): def register_mime(id, mimetype):
""" """
Registers an image MIME type. This function should not be used Registers an image MIME type by populating ``Image.MIME``. This function
in application code. should not be used in application code.
``Image.MIME`` provides a mapping from image format identifiers to mime
formats, but :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` can
provide a different result for specific images.
:param id: An image format identifier. :param id: An image format identifier.
:param mimetype: The image MIME type for this format. :param mimetype: The image MIME type for this format.
@ -3725,6 +3751,7 @@ class Exif(MutableMapping):
self.endian = self._info._endian self.endian = self._info._endian
if offset is None: if offset is None:
offset = self._info.next offset = self._info.next
self.fp.tell()
self.fp.seek(offset) self.fp.seek(offset)
self._info.load(self.fp) self._info.load(self.fp)

View File

@ -563,14 +563,21 @@ class FreeTypeFont:
if start is None: if start is None:
start = (0, 0) start = (0, 0)
im = None im = None
size = None
def fill(mode, size): def fill(mode, im_size):
nonlocal im nonlocal im, size
size = im_size
if Image.MAX_IMAGE_PIXELS is not None:
pixels = max(1, size[0]) * max(1, size[1])
if pixels > 2 * Image.MAX_IMAGE_PIXELS:
return
im = Image.core.fill(mode, size) im = Image.core.fill(mode, size)
return im return im
size, offset = self.font.render( offset = self.font.render(
text, text,
fill, fill,
mode, mode,
@ -582,7 +589,6 @@ class FreeTypeFont:
ink, ink,
start[0], start[0],
start[1], start[1],
Image.MAX_IMAGE_PIXELS,
) )
Image._decompression_bomb_check(size) Image._decompression_bomb_check(size)
return im, offset return im, offset

View File

@ -166,7 +166,7 @@ def grabclipboard():
msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
raise NotImplementedError(msg) raise NotImplementedError(msg)
p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) p = subprocess.run(args, capture_output=True)
err = p.stderr err = p.stderr
if err: if err:
msg = f"{args[0]} error: {err.strip().decode()}" msg = f"{args[0]} error: {err.strip().decode()}"

View File

@ -242,7 +242,7 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
Returns a resized version of the image, set to the maximum width and height Returns a resized version of the image, set to the maximum width and height
within the requested size, while maintaining the original aspect ratio. within the requested size, while maintaining the original aspect ratio.
:param image: The image to resize and crop. :param image: The image to resize.
:param size: The requested output size in pixels, given as a :param size: The requested output size in pixels, given as a
(width, height) tuple. (width, height) tuple.
:param method: Resampling method to use. Default is :param method: Resampling method to use. Default is
@ -266,6 +266,35 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
return image.resize(size, resample=method) return image.resize(size, resample=method)
def cover(image, size, method=Image.Resampling.BICUBIC):
"""
Returns a resized version of the image, so that the requested size is
covered, while maintaining the original aspect ratio.
:param image: The image to resize.
:param size: The requested output size in pixels, given as a
(width, height) tuple.
:param method: Resampling method to use. Default is
:py:attr:`~PIL.Image.Resampling.BICUBIC`.
See :ref:`concept-filters`.
:return: An image.
"""
im_ratio = image.width / image.height
dest_ratio = size[0] / size[1]
if im_ratio != dest_ratio:
if im_ratio < dest_ratio:
new_height = round(image.height / image.width * size[0])
if new_height != size[1]:
size = (size[0], new_height)
else:
new_width = round(image.width / image.height * size[1])
if new_width != size[0]:
size = (new_width, size[1])
return image.resize(size, resample=method)
def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)): def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)):
""" """
Returns a resized and padded version of the image, expanded to fill the Returns a resized and padded version of the image, expanded to fill the
@ -588,6 +617,7 @@ def exif_transpose(image, *, in_place=False):
with the transposition applied. If there is no transposition, a copy of the with the transposition applied. If there is no transposition, a copy of the
image will be returned. image will be returned.
""" """
image.load()
image_exif = image.getexif() image_exif = image.getexif()
orientation = image_exif.get(ExifTags.Base.Orientation) orientation = image_exif.get(ExifTags.Base.Orientation)
method = { method = {

View File

@ -58,13 +58,13 @@ class IptcImageFile(ImageFile.ImageFile):
# #
# get a IPTC field header # get a IPTC field header
s = self.fp.read(5) s = self.fp.read(5)
if not len(s): if not s.strip(b"\x00"):
return None, 0 return None, 0
tag = s[1], s[2] tag = s[1], s[2]
# syntax # syntax
if s[0] != 0x1C or tag[0] < 1 or tag[0] > 9: if s[0] != 0x1C or tag[0] not in [1, 2, 3, 4, 5, 6, 7, 8, 9, 240]:
msg = "invalid IPTC/NAA file" msg = "invalid IPTC/NAA file"
raise SyntaxError(msg) raise SyntaxError(msg)

View File

@ -170,11 +170,19 @@ def APP(self, marker):
# 1 dpcm = 2.54 dpi # 1 dpcm = 2.54 dpi
dpi *= 2.54 dpi *= 2.54
self.info["dpi"] = dpi, dpi self.info["dpi"] = dpi, dpi
except (TypeError, KeyError, SyntaxError, ValueError, ZeroDivisionError): except (
# SyntaxError for invalid/unreadable EXIF struct.error,
KeyError,
SyntaxError,
TypeError,
ValueError,
ZeroDivisionError,
):
# struct.error for truncated EXIF
# KeyError for dpi not included # KeyError for dpi not included
# ZeroDivisionError for invalid dpi rational value # SyntaxError for invalid/unreadable EXIF
# ValueError or TypeError for dpi being an invalid float # ValueError or TypeError for dpi being an invalid float
# ZeroDivisionError for invalid dpi rational value
self.info["dpi"] = 72, 72 self.info["dpi"] = 72, 72
@ -496,7 +504,7 @@ class JpegImageFile(ImageFile.ImageFile):
for segment, content in self.applist: for segment, content in self.applist:
if segment == "APP1": if segment == "APP1":
marker, xmp_tags = content.rsplit(b"\x00", 1) marker, xmp_tags = content.split(b"\x00")[:2]
if marker == b"http://ns.adobe.com/xap/1.0/": if marker == b"http://ns.adobe.com/xap/1.0/":
return self._getxmp(xmp_tags) return self._getxmp(xmp_tags)
return {} return {}

View File

@ -1042,6 +1042,7 @@ _OUTMODES = {
"LA": ("LA", b"\x08\x04"), "LA": ("LA", b"\x08\x04"),
"I": ("I;16B", b"\x10\x00"), "I": ("I;16B", b"\x10\x00"),
"I;16": ("I;16B", b"\x10\x00"), "I;16": ("I;16B", b"\x10\x00"),
"I;16B": ("I;16B", b"\x10\x00"),
"P;1": ("P;1", b"\x01\x03"), "P;1": ("P;1", b"\x01\x03"),
"P;2": ("P;2", b"\x02\x03"), "P;2": ("P;2", b"\x02\x03"),
"P;4": ("P;4", b"\x04\x03"), "P;4": ("P;4", b"\x04\x03"),
@ -1104,10 +1105,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
if im_frame.mode == rawmode: if im_frame.mode == rawmode:
im_frame = im_frame.copy() im_frame = im_frame.copy()
else: else:
if rawmode == "P": im_frame = im_frame.convert(rawmode)
im_frame = im_frame.convert(rawmode, palette=im.palette)
else:
im_frame = im_frame.convert(rawmode)
encoderinfo = im.encoderinfo.copy() encoderinfo = im.encoderinfo.copy()
if isinstance(duration, (list, tuple)): if isinstance(duration, (list, tuple)):
encoderinfo["duration"] = duration[frame_count] encoderinfo["duration"] = duration[frame_count]
@ -1166,6 +1164,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
# default image IDAT (if it exists) # default image IDAT (if it exists)
if default_image: if default_image:
if im.mode != rawmode:
im = im.convert(rawmode)
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
seq_num = 0 seq_num = 0
@ -1227,11 +1227,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
) )
modes = set() modes = set()
append_images = im.encoderinfo.get("append_images", []) append_images = im.encoderinfo.get("append_images", [])
if default_image: for im_seq in itertools.chain([im], append_images):
chain = itertools.chain(append_images)
else:
chain = itertools.chain([im], append_images)
for im_seq in chain:
for im_frame in ImageSequence.Iterator(im_seq): for im_frame in ImageSequence.Iterator(im_seq):
modes.add(im_frame.mode) modes.add(im_frame.mode)
for mode in ("RGBA", "RGB", "P"): for mode in ("RGBA", "RGB", "P"):

View File

@ -55,7 +55,7 @@ class QoiDecoder(ImageFile.PyDecoder):
while len(data) < self.state.xsize * self.state.ysize * bands: while len(data) < self.state.xsize * self.state.ysize * bands:
byte = self.fd.read(1)[0] byte = self.fd.read(1)[0]
if byte == 0b11111110: # QOI_OP_RGB if byte == 0b11111110: # QOI_OP_RGB
value = self.fd.read(3) + o8(255) value = self.fd.read(3) + self._previous_pixel[3:]
elif byte == 0b11111111: # QOI_OP_RGBA elif byte == 0b11111111: # QOI_OP_RGBA
value = self.fd.read(4) value = self.fd.read(4)
else: else:

Some files were not shown because too many files have changed in this diff Show More