Compare commits
182 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
627d8743b7 | ||
|
|
dcd52ebf65 | ||
|
|
d6e0a8d174 | ||
|
|
2210714a43 | ||
|
|
3d7801417a | ||
|
|
a85d3b135d | ||
|
|
932aa68d2a | ||
|
|
fe236d77a5 | ||
|
|
bc0e2c0e61 | ||
|
|
e66dd607f0 | ||
|
|
d5d8a91597 | ||
|
|
b8351fde41 | ||
|
|
36cf82ae76 | ||
|
|
525842215f | ||
|
|
844b10f894 | ||
|
|
555fb8371c | ||
|
|
0a1d6c3c61 | ||
|
|
00ec73dfd1 | ||
|
|
e924cfd181 | ||
|
|
2360d0df17 | ||
|
|
499b796556 | ||
|
|
5b677ca1c6 | ||
|
|
b71109d435 | ||
|
|
4337139f0c | ||
|
|
46f45f674d | ||
|
|
c9ac097edb | ||
|
|
3baedf2648 | ||
|
|
b51a036685 | ||
|
|
8d08e31533 | ||
|
|
432707ea81 | ||
|
|
2d589107fb | ||
|
|
8dee8dd5ba | ||
|
|
b2d9bc3c76 | ||
|
|
f130c10a9c | ||
|
|
ce11a0c499 | ||
|
|
51b35d17e1 | ||
|
|
a868c29eb1 | ||
|
|
43f8efad79 | ||
|
|
91f219fdcf | ||
|
|
900636e7db | ||
|
|
d62955031b | ||
|
|
2ebfe30ae3 | ||
|
|
19910ed03e | ||
|
|
6b892c495c | ||
|
|
0a9a47fb9b | ||
|
|
15c9d11f35 | ||
|
|
81e80f7a50 | ||
|
|
72931475f2 | ||
|
|
79357a2718 | ||
|
|
3abb62ed29 | ||
|
|
080afe1bf7 | ||
|
|
2ebb3e9964 | ||
|
|
a04c9806b1 | ||
|
|
faa843e9c2 | ||
|
|
66e3d65a72 | ||
|
|
4be5b8a2fb | ||
|
|
e85700fe48 | ||
|
|
a704711404 | ||
|
|
9b7200d2b4 | ||
|
|
ca21683316 | ||
|
|
4cbef1667f | ||
|
|
9dd756f9fe | ||
|
|
00e2198eeb | ||
|
|
9d3555c37e | ||
|
|
6df6cd4480 | ||
|
|
205e52b1ee | ||
|
|
6bf4313a68 | ||
|
|
8494b06c71 | ||
|
|
6a769da21b | ||
|
|
2c6fd36f10 | ||
|
|
c0b8c2f0a2 | ||
|
|
79ae888d45 | ||
|
|
b3da65df94 | ||
|
|
1f424efd25 | ||
|
|
374957cefd | ||
|
|
6d493aa817 | ||
|
|
33204aac4d | ||
|
|
4eb7cd6f29 | ||
|
|
76532808f4 | ||
|
|
3332c1d82e | ||
|
|
b3d7263f74 | ||
|
|
a01fa7d08e | ||
|
|
db7a994ad6 | ||
|
|
07fee96880 | ||
|
|
fd1ddd6d56 | ||
|
|
7c3ece07c9 | ||
|
|
61b1c3c841 | ||
|
|
46ac30aa80 | ||
|
|
4024f0287d | ||
|
|
4d511d86ed | ||
|
|
fd3d44d2ef | ||
|
|
24b1702360 | ||
|
|
ae45187719 | ||
|
|
b633f49b9c | ||
|
|
7adecb792c | ||
|
|
dbc5a4fc90 | ||
|
|
b3d9ba8e88 | ||
|
|
47c6aae0ca | ||
|
|
9342e209b2 | ||
|
|
ce3e085751 | ||
|
|
b0a5bc2a6b | ||
|
|
370da461cf | ||
|
|
77e16b1030 | ||
|
|
2150f088ed | ||
|
|
972bb4c39b | ||
|
|
c9095cb02a | ||
|
|
416f02338b | ||
|
|
89795df94f | ||
|
|
93aa55cece | ||
|
|
8d7dc9db5b | ||
|
|
4a733e5092 | ||
|
|
da76f6d99b | ||
|
|
65c32ecca4 | ||
|
|
5543e85ad2 | ||
|
|
37da2ba381 | ||
|
|
8814d42fd9 | ||
|
|
d06c8b3591 | ||
|
|
6a9960e8c1 | ||
|
|
ec40c546d7 | ||
|
|
7055937eb1 | ||
|
|
cce73b1e89 | ||
|
|
88247a9ef3 | ||
|
|
71b3e5c015 | ||
|
|
75280b8b0f | ||
|
|
6107b9e82d | ||
|
|
38c6c478e0 | ||
|
|
bc1237ef3d | ||
|
|
142c1320b2 | ||
|
|
3b8fd040de | ||
|
|
8fbb801275 | ||
|
|
921a470506 | ||
|
|
b33a31524a | ||
|
|
18c7f87fe3 | ||
|
|
e44ce2f00e | ||
|
|
b59e031256 | ||
|
|
666dd52478 | ||
|
|
85d783fb52 | ||
|
|
b3d9bd9950 | ||
|
|
1a27f958d7 | ||
|
|
dfd24ba615 | ||
|
|
e36e67081a | ||
|
|
b90a00eccb | ||
|
|
ce3323afa9 | ||
|
|
29c5ffe745 | ||
|
|
cc4ca5bf17 | ||
|
|
148a19eee4 | ||
|
|
a63ba0e3b6 | ||
|
|
82cdaa456c | ||
|
|
ddd4f00720 | ||
|
|
b04d8792f5 | ||
|
|
109ee1569d | ||
|
|
208bbe95f9 | ||
|
|
b1e2f2e652 | ||
|
|
7d6f2ce90b | ||
|
|
e1f4352ce9 | ||
|
|
e90bb1559c | ||
|
|
4b90888a7d | ||
|
|
51e3fe45bf | ||
|
|
76f04b46c5 | ||
|
|
2d23257595 | ||
|
|
03d48f4011 | ||
|
|
e969fa7aea | ||
|
|
ae43b36030 | ||
|
|
5122c8356d | ||
|
|
424168b69c | ||
|
|
ae7d28eddb | ||
|
|
933df2450d | ||
|
|
3620d48459 | ||
|
|
c680ff029f | ||
|
|
7d89946688 | ||
|
|
3eecafd62c | ||
|
|
ca3528f46e | ||
|
|
610d564aea | ||
|
|
53302c2281 | ||
|
|
79914ec8a5 | ||
|
|
7eaac3fcf0 | ||
|
|
1f7e9c3b51 | ||
|
|
5ce88dbe53 | ||
|
|
49efe40f28 | ||
|
|
9f0614de7e | ||
|
|
a666057989 | ||
|
|
349cc44fd4 |
|
|
@ -27,14 +27,13 @@ python3 -m pip install --upgrade wheel
|
|||
python3 -m pip install coverage
|
||||
python3 -m pip install defusedxml
|
||||
python3 -m pip install ipython
|
||||
python3 -m pip install numpy
|
||||
python3 -m pip install olefile
|
||||
python3 -m pip install -U pytest
|
||||
python3 -m pip install -U pytest-cov
|
||||
python3 -m pip install -U pytest-timeout
|
||||
python3 -m pip install pyroma
|
||||
# optional test dependency, only install if there's a binary package.
|
||||
# fails on beta 3.14 and PyPy
|
||||
# optional test dependencies, only install if there's a binary package.
|
||||
python3 -m pip install --only-binary=:all: numpy || true
|
||||
python3 -m pip install --only-binary=:all: pyarrow || true
|
||||
|
||||
# PyQt6 doesn't support PyPy3
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
cibuildwheel==3.2.1
|
||||
cibuildwheel==3.3.0
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
mypy==1.18.2
|
||||
mypy==1.19.1
|
||||
arro3-compute
|
||||
arro3-core
|
||||
IceSpringPySideStubs-PyQt6
|
||||
|
|
@ -9,7 +9,6 @@ packaging
|
|||
pyarrow-stubs
|
||||
pybind11
|
||||
pytest
|
||||
sphinx
|
||||
types-atheris
|
||||
types-defusedxml
|
||||
types-olefile
|
||||
|
|
|
|||
1
.github/renovate.json
vendored
|
|
@ -6,6 +6,7 @@
|
|||
"labels": [
|
||||
"Dependency"
|
||||
],
|
||||
"minimumReleaseAge": "7 days",
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "github-actions",
|
||||
|
|
|
|||
4
.github/workflows/cifuzz.yml
vendored
|
|
@ -44,13 +44,13 @@ jobs:
|
|||
language: python
|
||||
dry-run: false
|
||||
- name: Upload New Crash
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
if: failure() && steps.build.outcome == 'success'
|
||||
with:
|
||||
name: artifacts
|
||||
path: ./out/artifacts
|
||||
- name: Upload Legacy Crash
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
if: steps.run.outcome == 'success'
|
||||
with:
|
||||
name: crash
|
||||
|
|
|
|||
4
.github/workflows/docs.yml
vendored
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
name: Docs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ jobs:
|
|||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: Cache libimagequant
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
id: cache-libimagequant
|
||||
with:
|
||||
path: ~/cache-libimagequant
|
||||
|
|
|
|||
58
.github/workflows/lint.yml
vendored
|
|
@ -2,55 +2,31 @@ name: Lint
|
|||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
PREK_COLOR: always
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
name: Lint
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: pre-commit cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }}
|
||||
restore-keys: |
|
||||
lint-pre-commit-
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
cache: pip
|
||||
cache-dependency-path: "setup.py"
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python3 -m pip install -U pip
|
||||
python3 -m pip install -U tox
|
||||
|
||||
- name: Lint
|
||||
run: tox -e lint
|
||||
env:
|
||||
PRE_COMMIT_COLOR: always
|
||||
|
||||
- name: Mypy
|
||||
run: tox -e mypy
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Lint
|
||||
run: uvx --with tox-uv tox -e lint
|
||||
- name: Mypy
|
||||
run: uvx --with tox-uv tox -e mypy
|
||||
|
|
|
|||
5
.github/workflows/macos-install.sh
vendored
|
|
@ -26,9 +26,8 @@ python3 -m pip install -U pytest
|
|||
python3 -m pip install -U pytest-cov
|
||||
python3 -m pip install -U pytest-timeout
|
||||
python3 -m pip install pyroma
|
||||
python3 -m pip install numpy
|
||||
# optional test dependency, only install if there's a binary package.
|
||||
# fails on beta 3.14 and PyPy
|
||||
# optional test dependencies, only install if there's a binary package.
|
||||
python3 -m pip install --only-binary=:all: numpy || true
|
||||
python3 -m pip install --only-binary=:all: pyarrow || true
|
||||
|
||||
# libavif
|
||||
|
|
|
|||
4
.github/workflows/test-docker.yml
vendored
|
|
@ -49,8 +49,8 @@ jobs:
|
|||
debian-12-bookworm-amd64,
|
||||
debian-13-trixie-x86,
|
||||
debian-13-trixie-amd64,
|
||||
fedora-41-amd64,
|
||||
fedora-42-amd64,
|
||||
fedora-43-amd64,
|
||||
gentoo,
|
||||
ubuntu-22.04-jammy-amd64,
|
||||
ubuntu-24.04-noble-amd64,
|
||||
|
|
@ -68,7 +68,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
|
|||
2
.github/workflows/test-mingw.yml
vendored
|
|
@ -45,7 +45,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
|
|||
2
.github/workflows/test-valgrind-memory.yml
vendored
|
|
@ -41,7 +41,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
|
|||
2
.github/workflows/test-valgrind.yml
vendored
|
|
@ -39,7 +39,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
|
|||
26
.github/workflows/test-windows.yml
vendored
|
|
@ -31,15 +31,16 @@ env:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14"]
|
||||
python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14", "3.15"]
|
||||
architecture: ["x64"]
|
||||
os: ["windows-latest"]
|
||||
include:
|
||||
# Test the oldest Python on 32-bit
|
||||
- { python-version: "3.10", architecture: "x86" }
|
||||
- { python-version: "3.10", architecture: "x86", os: "windows-2022" }
|
||||
|
||||
timeout-minutes: 45
|
||||
|
||||
|
|
@ -47,19 +48,19 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout cached dependencies
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: python-pillow/pillow-depends
|
||||
path: winbuild\depends
|
||||
|
||||
- name: Checkout extra test images
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: python-pillow/test-images
|
||||
|
|
@ -83,7 +84,7 @@ jobs:
|
|||
python3 -m pip install --upgrade pip
|
||||
|
||||
- name: Install CPython dependencies
|
||||
if: "!contains(matrix.python-version, 'pypy') && !contains(matrix.python-version, '3.14') && matrix.architecture != 'x86'"
|
||||
if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'"
|
||||
run: |
|
||||
python3 -m pip install PyQt6
|
||||
|
||||
|
|
@ -111,7 +112,7 @@ jobs:
|
|||
|
||||
- name: Cache build
|
||||
id: build-cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: winbuild\build
|
||||
key:
|
||||
|
|
@ -187,8 +188,9 @@ jobs:
|
|||
# trim ~150MB for each job
|
||||
- name: Optimize build cache
|
||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||
run: rmdir /S /Q winbuild\build\src
|
||||
shell: cmd
|
||||
run: |
|
||||
rm -rf winbuild\build\src
|
||||
shell: bash
|
||||
|
||||
- name: Build Pillow
|
||||
run: |
|
||||
|
|
@ -205,9 +207,7 @@ jobs:
|
|||
|
||||
- name: Test Pillow
|
||||
run: |
|
||||
path %GITHUB_WORKSPACE%\winbuild\build\bin;%PATH%
|
||||
.ci\test.cmd
|
||||
shell: cmd
|
||||
|
||||
- name: Prepare to upload errors
|
||||
if: failure()
|
||||
|
|
@ -216,7 +216,7 @@ jobs:
|
|||
shell: bash
|
||||
|
||||
- name: Upload errors
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
if: failure()
|
||||
with:
|
||||
name: errors
|
||||
|
|
|
|||
9
.github/workflows/test.yml
vendored
|
|
@ -42,6 +42,8 @@ jobs:
|
|||
]
|
||||
python-version: [
|
||||
"pypy3.11",
|
||||
"3.15t",
|
||||
"3.15",
|
||||
"3.14t",
|
||||
"3.14",
|
||||
"3.13t",
|
||||
|
|
@ -54,6 +56,7 @@ jobs:
|
|||
- { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
|
||||
- { python-version: "3.11", PYTHONOPTIMIZE: 2 }
|
||||
# Free-threaded
|
||||
- { python-version: "3.15t", disable-gil: true }
|
||||
- { python-version: "3.14t", disable-gil: true }
|
||||
- { python-version: "3.13t", disable-gil: true }
|
||||
# Intel
|
||||
|
|
@ -65,7 +68,7 @@ jobs:
|
|||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
@ -89,7 +92,7 @@ jobs:
|
|||
|
||||
- name: Cache libimagequant
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
id: cache-libimagequant
|
||||
with:
|
||||
path: ~/cache-libimagequant
|
||||
|
|
@ -140,7 +143,7 @@ jobs:
|
|||
mkdir -p Tests/errors
|
||||
|
||||
- name: Upload errors
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
if: failure()
|
||||
with:
|
||||
name: errors
|
||||
|
|
|
|||
32
.github/workflows/wheels-dependencies.sh
vendored
|
|
@ -32,7 +32,6 @@ if [[ "$CIBW_PLATFORM" == "ios" ]]; then
|
|||
# or `build/deps/iphonesimulator`
|
||||
WORKDIR=$(pwd)/build/$IOS_SDK
|
||||
BUILD_PREFIX=$(pwd)/build/deps/$IOS_SDK
|
||||
PATCH_DIR=$(pwd)/patches/iOS
|
||||
|
||||
# GNU tooling insists on using aarch64 rather than arm64
|
||||
if [[ $PLAT == "arm64" ]]; then
|
||||
|
|
@ -90,27 +89,25 @@ fi
|
|||
|
||||
ARCHIVE_SDIR=pillow-depends-main
|
||||
|
||||
# Package versions for fresh source builds. Version numbers with "Patched"
|
||||
# annotations have a source code patch that is required for some platforms. If
|
||||
# you change those versions, ensure the patch is also updated.
|
||||
# Package versions for fresh source builds.
|
||||
if [[ -n "$IOS_SDK" ]]; then
|
||||
FREETYPE_VERSION=2.13.3
|
||||
else
|
||||
FREETYPE_VERSION=2.14.1
|
||||
fi
|
||||
HARFBUZZ_VERSION=12.1.0
|
||||
LIBPNG_VERSION=1.6.50
|
||||
JPEGTURBO_VERSION=3.1.2
|
||||
HARFBUZZ_VERSION=12.3.0
|
||||
LIBPNG_VERSION=1.6.53
|
||||
JPEGTURBO_VERSION=3.1.3
|
||||
OPENJPEG_VERSION=2.5.4
|
||||
XZ_VERSION=5.8.1
|
||||
XZ_VERSION=5.8.2
|
||||
ZSTD_VERSION=1.5.7
|
||||
TIFF_VERSION=4.7.1
|
||||
LCMS2_VERSION=2.17
|
||||
ZLIB_NG_VERSION=2.2.5
|
||||
ZLIB_NG_VERSION=2.3.2
|
||||
LIBWEBP_VERSION=1.6.0
|
||||
BZIP2_VERSION=1.0.8
|
||||
LIBXCB_VERSION=1.17.0
|
||||
BROTLI_VERSION=1.1.0 # Patched; next release won't need patching. See patch file.
|
||||
BROTLI_VERSION=1.2.0
|
||||
LIBAVIF_VERSION=1.3.0
|
||||
|
||||
function build_pkg_config {
|
||||
|
|
@ -149,18 +146,9 @@ function build_zlib_ng {
|
|||
ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS
|
||||
unset HOST_CONFIGURE_FLAGS
|
||||
|
||||
build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat
|
||||
build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat
|
||||
|
||||
HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS
|
||||
|
||||
if [[ -n "$IS_MACOS" ]] && [[ -z "$IOS_SDK" ]]; then
|
||||
# Ensure that on macOS, the library name is an absolute path, not an
|
||||
# @rpath, so that delocate picks up the right library (and doesn't need
|
||||
# DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an
|
||||
# option to control the install_name. This isn't needed on iOS, as iOS
|
||||
# only builds the static library.
|
||||
install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib
|
||||
fi
|
||||
touch zlib-stamp
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +156,7 @@ function build_brotli {
|
|||
if [ -e brotli-stamp ]; then return; fi
|
||||
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
|
||||
(cd $out_dir \
|
||||
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \
|
||||
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib -DCMAKE_MACOSX_BUNDLE=OFF $HOST_CMAKE_FLAGS . \
|
||||
&& make -j4 install)
|
||||
touch brotli-stamp
|
||||
}
|
||||
|
|
@ -279,7 +267,7 @@ function build {
|
|||
|
||||
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
|
||||
build_simple xorgproto 2025.1 https://www.x.org/pub/individual/proto
|
||||
build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib
|
||||
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
||||
else
|
||||
|
|
|
|||
58
.github/workflows/wheels.yml
vendored
|
|
@ -100,14 +100,14 @@ jobs:
|
|||
cibw_arch: arm64_iphoneos
|
||||
- name: "iOS arm64 simulator"
|
||||
platform: ios
|
||||
os: macos-14
|
||||
os: macos-latest
|
||||
cibw_arch: arm64_iphonesimulator
|
||||
- name: "iOS x86_64 simulator"
|
||||
platform: ios
|
||||
os: macos-15-intel
|
||||
cibw_arch: x86_64_iphonesimulator
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
|
|
@ -134,7 +134,7 @@ jobs:
|
|||
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: dist-${{ matrix.name }}
|
||||
path: ./wheelhouse/*.whl
|
||||
|
|
@ -154,12 +154,12 @@ jobs:
|
|||
- cibw_arch: ARM64
|
||||
os: windows-11-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout extra test images
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: python-pillow/test-images
|
||||
|
|
@ -186,24 +186,18 @@ jobs:
|
|||
|
||||
- name: Build wheels
|
||||
run: |
|
||||
setlocal EnableDelayedExpansion
|
||||
for %%f in (winbuild\build\license\*) do (
|
||||
set x=%%~nf
|
||||
rem Skip FriBiDi license, it is not included in the wheel.
|
||||
set fribidi=!x:~0,7!
|
||||
if NOT !fribidi!==fribidi (
|
||||
rem Skip imagequant license, it is not included in the wheel.
|
||||
set libimagequant=!x:~0,13!
|
||||
if NOT !libimagequant!==libimagequant (
|
||||
echo. >> LICENSE
|
||||
echo ===== %%~nf ===== >> LICENSE
|
||||
echo. >> LICENSE
|
||||
type %%f >> LICENSE
|
||||
)
|
||||
)
|
||||
)
|
||||
call winbuild\\build\\build_env.cmd
|
||||
%pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse
|
||||
for f in winbuild/build/license/*; do
|
||||
name=$(basename "${f%.*}")
|
||||
# Skip FriBiDi license, it is not included in the wheel.
|
||||
[[ $name == fribidi* ]] && continue
|
||||
# Skip imagequant license, it is not included in the wheel.
|
||||
[[ $name == libimagequant* ]] && continue
|
||||
echo "" >> LICENSE
|
||||
echo "===== $name =====" >> LICENSE
|
||||
echo "" >> LICENSE
|
||||
cat "$f" >> LICENSE
|
||||
done
|
||||
cmd //c "winbuild\\build\\build_env.cmd && $pythonLocation\\python.exe -m cibuildwheel . --output-dir wheelhouse"
|
||||
env:
|
||||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
||||
|
|
@ -217,16 +211,16 @@ jobs:
|
|||
-e CI -e GITHUB_ACTIONS
|
||||
mcr.microsoft.com/windows/servercore:ltsc2022
|
||||
powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test'
|
||||
shell: cmd
|
||||
shell: bash
|
||||
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: dist-windows-${{ matrix.cibw_arch }}
|
||||
path: ./wheelhouse/*.whl
|
||||
|
||||
- name: Upload fribidi.dll
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: fribidi-windows-${{ matrix.cibw_arch }}
|
||||
path: winbuild\build\bin\fribidi*
|
||||
|
|
@ -235,7 +229,7 @@ jobs:
|
|||
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
@ -246,7 +240,7 @@ jobs:
|
|||
|
||||
- run: make sdist
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: dist-sdist
|
||||
path: dist/*.tar.gz
|
||||
|
|
@ -256,7 +250,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
name: Count dists
|
||||
steps:
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: dist-*
|
||||
path: dist
|
||||
|
|
@ -275,13 +269,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
name: Upload wheels to scientific-python-nightly-wheels
|
||||
steps:
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: dist-!(sdist)*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Upload wheels to scientific-python-nightly-wheels
|
||||
uses: scientific-python/upload-nightly-action@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2
|
||||
uses: scientific-python/upload-nightly-action@5748273c71e2d8d3a61f3a11a16421c8954f9ecf # 0.6.3
|
||||
with:
|
||||
artifacts_path: dist
|
||||
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
|
||||
|
|
@ -297,7 +291,7 @@ jobs:
|
|||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v5
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: dist-*
|
||||
path: dist
|
||||
|
|
|
|||
1
.github/zizmor.yml
vendored
|
|
@ -1,4 +1,3 @@
|
|||
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
|
||||
# https://docs.zizmor.sh/configuration/
|
||||
rules:
|
||||
unpinned-uses:
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.13.3
|
||||
rev: v0.14.10
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args: [--exit-non-zero-on-fix]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 25.9.0
|
||||
rev: 25.12.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.8.6
|
||||
rev: 1.9.2
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: [--severity-level=high]
|
||||
|
|
@ -21,10 +21,10 @@ repos:
|
|||
rev: v1.5.5
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v21.1.2
|
||||
rev: v21.1.8
|
||||
hooks:
|
||||
- id: clang-format
|
||||
types: [c]
|
||||
|
|
@ -46,29 +46,29 @@ repos:
|
|||
- id: check-yaml
|
||||
args: [--allow-multiple-documents]
|
||||
- id: end-of-file-fixer
|
||||
exclude: ^Tests/images/|\.patch$
|
||||
exclude: ^Tests/images/
|
||||
- id: trailing-whitespace
|
||||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$
|
||||
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.34.0
|
||||
rev: 0.36.0
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
- id: check-readthedocs
|
||||
- id: check-renovate
|
||||
|
||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||
rev: v1.14.2
|
||||
rev: v1.19.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v1.0.0
|
||||
rev: v1.0.2
|
||||
hooks:
|
||||
- id: sphinx-lint
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: v2.7.0
|
||||
rev: v2.11.1
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ repos:
|
|||
additional_dependencies: [trove-classifiers>=2024.10.12]
|
||||
|
||||
- repo: https://github.com/tox-dev/tox-ini-fmt
|
||||
rev: 1.6.0
|
||||
rev: 1.7.1
|
||||
hooks:
|
||||
- id: tox-ini-fmt
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ include tox.ini
|
|||
graft Tests
|
||||
graft Tests/images
|
||||
graft checks
|
||||
graft patches
|
||||
graft src
|
||||
graft depends
|
||||
graft winbuild
|
||||
|
|
|
|||
BIN
Tests/fonts/AdobeVFPrototypeDuplicates.ttf
Normal file
|
|
@ -2,7 +2,7 @@
|
|||
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
|
||||
NotoSans-Regular.ttf, from https://www.google.com/get/noto/
|
||||
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
|
||||
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
|
||||
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype. AdobeVFPrototypeDuplicates.ttf is a modified version of this
|
||||
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
|
||||
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
|
||||
ter-x20b.pcf, from http://terminus-font.sourceforge.net/
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ def convert_to_comparable(
|
|||
if a.mode == "P":
|
||||
new_a = Image.new("L", a.size)
|
||||
new_b = Image.new("L", b.size)
|
||||
new_a.putdata(a.getdata())
|
||||
new_b.putdata(b.getdata())
|
||||
new_a.putdata(a.get_flattened_data())
|
||||
new_b.putdata(b.get_flattened_data())
|
||||
elif a.mode == "I;16":
|
||||
new_a = a.convert("I")
|
||||
new_b = b.convert("I")
|
||||
|
|
@ -104,10 +104,9 @@ def assert_image_equal_tofile(
|
|||
msg: str | None = None,
|
||||
mode: str | None = None,
|
||||
) -> None:
|
||||
with Image.open(filename) as img:
|
||||
if mode:
|
||||
img = img.convert(mode)
|
||||
assert_image_equal(a, img, msg)
|
||||
with Image.open(filename) as im:
|
||||
converted_im = im.convert(mode) if mode else im
|
||||
assert_image_equal(a, converted_im, msg)
|
||||
|
||||
|
||||
def assert_image_similar(
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 126 B |
|
|
@ -1,578 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
|
||||
<title>BMP Suite Image List</title>
|
||||
|
||||
<style>
|
||||
.b { background:url(bkgd.png); }
|
||||
.q { background-color:#fff0e0; }
|
||||
.bad { background-color:#ffa0a0; }
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>BMP Suite Image List</h1>
|
||||
|
||||
<p><i>For <a href="http://entropymine.com/jason/bmpsuite/">BMP Suite</a>
|
||||
version 2.3</i></p>
|
||||
|
||||
<p>This document describes the images in <i>BMP Suite</i>, and shows what
|
||||
I allege to be the correct way to interpret them. PNG and JPEG images are
|
||||
used for reference.
|
||||
</p>
|
||||
|
||||
<p>It also shows how your web browser displays the BMP images,
|
||||
but that’s not its main purpose.
|
||||
BMP is poor image format to use on web pages, so a web browser’s
|
||||
level of support for it is arguably not important.</p>
|
||||
|
||||
<table border=1 cellpadding=8>
|
||||
|
||||
<tr>
|
||||
<th>File</th>
|
||||
<th>Ver.</th>
|
||||
<th>Correct display</th>
|
||||
<th>In your browser</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal1.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal1.png"></td>
|
||||
<td class=b><img src="../g/pal1.bmp"></td>
|
||||
<td>1 bit/pixel paletted image, in which black is the first color in
|
||||
the palette.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal1wb.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal1.png"></td>
|
||||
<td class=b><img src="../g/pal1wb.bmp"></td>
|
||||
<td>1 bit/pixel paletted image, in which white is the first color in
|
||||
the palette.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal1bg.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal1bg.png"></td>
|
||||
<td class=b><img src="../g/pal1bg.bmp"></td>
|
||||
<td>1 bit/pixel paletted image, with colors other than black and white.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/pal1p1.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal1p1.png"></td>
|
||||
<td class=b><img src="../q/pal1p1.bmp"></td>
|
||||
<td>1 bit/pixel paletted image, with only one color in the palette.
|
||||
The documentation says that 1-bpp images have a palette size of 2
|
||||
(not “up to 2”), but it would be silly for a viewer not to
|
||||
support a size of 1.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/pal2.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal2.png"></td>
|
||||
<td class=b><img src="../q/pal2.bmp"></td>
|
||||
<td>A paletted image with 2 bits/pixel. Usually only 1, 4,
|
||||
and 8 are allowed, but 2 is legal on Windows CE.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal4.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal4.png"></td>
|
||||
<td class=b><img src="../g/pal4.bmp"></td>
|
||||
<td>Paletted image with 12 palette colors, and 4 bits/pixel.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal4rle.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal4.png"></td>
|
||||
<td class=b><img src="../g/pal4rle.bmp"></td>
|
||||
<td>4-bit image that uses RLE compression.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/pal4rletrns.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal4rletrns.png"><br>
|
||||
or<br><img src="pal4rletrns-0.png"><br>
|
||||
or<br><img src="pal4rletrns-b.png"></td>
|
||||
<td class=b><img src="../q/pal4rletrns.bmp"></td>
|
||||
<td>An RLE-compressed image that used “delta”
|
||||
codes to skip over some pixels, leaving them undefined. Some viewers
|
||||
make undefined pixels transparent, others make them black, and
|
||||
others assign them palette color 0 (purple, in this case).</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal8.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal8.png"></td>
|
||||
<td class=b><img src="../g/pal8.bmp"></td>
|
||||
<td>Our standard paletted image, with 252 palette colors, and 8
|
||||
bits/pixel.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal8-0.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal8.png"></td>
|
||||
<td class=b><img src="../g/pal8-0.bmp"></td>
|
||||
<td>Every field that can be set to 0 is set to 0: pixels/meter=0;
|
||||
colors used=0 (meaning the default 256); size-of-image=0.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal8rle.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal8.png"></td>
|
||||
<td class=b><img src="../g/pal8rle.bmp"></td>
|
||||
<td>8-bit image that uses RLE compression.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/pal8rletrns.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal8rletrns.png"><br>
|
||||
or<br><img src="pal8rletrns-0.png"><br>
|
||||
or<br><img src="pal8rletrns-b.png"></td>
|
||||
<td class=b><img src="../q/pal8rletrns.bmp"></td>
|
||||
<td>8-bit version of q/pal4rletrns.bmp.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal8w126.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal8w126.png"></td>
|
||||
<td class=b><img src="../g/pal8w126.bmp"></td>
|
||||
<td rowspan=3>Images with different widths and heights.
|
||||
In BMP format, rows are padded to a multiple of four bytes, so we
|
||||
test all four possibilities.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal8w125.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal8w125.png"></td>
|
||||
<td class=b><img src="../g/pal8w125.bmp"></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal8w124.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal8w124.png"></td>
|
||||
<td class=b><img src="../g/pal8w124.bmp"></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal8topdown.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal8.png"></td>
|
||||
<td class=b><img src="../g/pal8topdown.bmp"></td>
|
||||
<td>BMP images are normally stored from the bottom up, but
|
||||
there is a way to store them from the top down.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/pal8offs.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal8.png"></td>
|
||||
<td class=b><img src="../q/pal8offs.bmp"></td>
|
||||
<td>A file with some unused bytes between the palette and the
|
||||
image. This is probably valid, but I’m not 100% sure.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/pal8oversizepal.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="pal8.png"></td>
|
||||
<td class=b><img src="../q/pal8oversizepal.bmp"></td>
|
||||
<td>An 8-bit image with 300 palette colors. This may be invalid,
|
||||
because the documentation could
|
||||
be interpreted to imply that 8-bit images aren’t allowed
|
||||
to have more than 256 colors.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal8nonsquare.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>
|
||||
<img src="pal8nonsquare-v.png"><br>
|
||||
or<br>
|
||||
<img src="pal8nonsquare-e.png">
|
||||
</td>
|
||||
<td class=b><img src="../g/pal8nonsquare.bmp"></td>
|
||||
<td>An image with non-square pixels: the X pixels/meter is twice
|
||||
the Y pixels/meter. Image <i>editors</i> can be expected to
|
||||
leave the image “squashed”; image <i>viewers</i> should
|
||||
consider stretching it to its correct proportions.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal8os2.bmp</td>
|
||||
<td>OS/2v1</td>
|
||||
<td class=b><img src="pal8.png"></td>
|
||||
<td class=b><img src="../g/pal8os2.bmp"></td>
|
||||
<td>An OS/2-style bitmap.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/pal8os2sp.bmp</td>
|
||||
<td>OS/2v1</td>
|
||||
<td class=b><img src="pal8.png"></td>
|
||||
<td class=b><img src="../q/pal8os2sp.bmp"></td>
|
||||
<td>An OS/2v1 with a less-than-full-sized palette.
|
||||
Probably not valid, but such files have been seen in the wild.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/pal8os2v2.bmp</td>
|
||||
<td>OS/2v2</td>
|
||||
<td class=b><img src="pal8.png"></td>
|
||||
<td class=b><img src="../q/pal8os2v2.bmp"></td>
|
||||
<td>My attempt to make an OS/2v2 bitmap.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/pal8os2v2-16.bmp</td>
|
||||
<td>OS/2v2</td>
|
||||
<td class=b><img src="pal8.png"></td>
|
||||
<td class=b><img src="../q/pal8os2v2-16.bmp"></td>
|
||||
<td>An OS/2v2 bitmap whose header has only 16 bytes, instead of the full 64.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal8v4.bmp</td>
|
||||
<td>4</td>
|
||||
<td class=b><img src="pal8.png"></td>
|
||||
<td class=b><img src="../g/pal8v4.bmp"></td>
|
||||
<td>A v4 bitmap. I’m not sure that the gamma and chromaticity values in
|
||||
this file are sensible, because I can’t find any detailed documentation
|
||||
of them.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/pal8v5.bmp</td>
|
||||
<td>5</td>
|
||||
<td class=b><img src="pal8.png"></td>
|
||||
<td class=b><img src="../g/pal8v5.bmp"></td>
|
||||
<td>A v5 bitmap. Version 5 has additional colorspace options over v4, so it
|
||||
is easier to create, and ought to be more portable.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/rgb16.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="rgb16.png"></td>
|
||||
<td class=b><img src="../g/rgb16.bmp"></td>
|
||||
<td>A 16-bit image with the default color format: 5 bits each for red,
|
||||
green, and blue, and 1 unused bit.
|
||||
The whitest colors should (I assume) be displayed as pure white:
|
||||
<span style="background-color:rgb(255,255,255)">(255,255,255)</span>, not
|
||||
<span style="background-color:rgb(248,248,248)">(248,248,248)</span>.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/rgb16-565.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="rgb16-565.png"></td>
|
||||
<td class=b><img src="../g/rgb16-565.bmp"></td>
|
||||
<td>A 16-bit image with a BITFIELDS segment indicating 5 red, 6 green,
|
||||
and 5 blue bits. This is a standard 16-bit format, even supported by
|
||||
old versions of Windows that don’t support any other non-default 16-bit
|
||||
formats.
|
||||
The whitest colors should be displayed as pure white:
|
||||
<span style="background-color:rgb(255,255,255)">(255,255,255)</span>, not
|
||||
<span style="background-color:rgb(248,252,248)">(248,252,248)</span>.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/rgb16-565pal.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="rgb16-565.png"></td>
|
||||
<td class=b><img src="../g/rgb16-565pal.bmp"></td>
|
||||
<td>A 16-bit image with both a BITFIELDS segment and a palette.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/rgb16-231.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="rgb16-231.png"></td>
|
||||
<td class=b><img src="../q/rgb16-231.bmp"></td>
|
||||
<td>An unusual and silly 16-bit image, with 2 red bits, 3 green bits, and 1
|
||||
blue bit. Most viewers do support this image, but the colors may be darkened
|
||||
with a yellow-green shadow. That’s because they’re doing simple
|
||||
bit-shifting (possibly including one round of bit replication), instead of
|
||||
proper scaling.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/rgba16-4444.bmp</td>
|
||||
<td>5</td>
|
||||
<td class=b><img src="rgba16-4444.png"></td>
|
||||
<td class=b><img src="../q/rgba16-4444.bmp"></td>
|
||||
<td>A 16-bit image with an alpha channel. There are 4 bits for each color
|
||||
channel, and 4 bits for the alpha channel.
|
||||
It’s not clear if this is valid, but I can’t find anything that
|
||||
suggests it isn’t.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/rgb24.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="rgb24.png"></td>
|
||||
<td class=b><img src="../g/rgb24.bmp"></td>
|
||||
<td>A perfectly ordinary 24-bit (truecolor) image.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/rgb24pal.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="rgb24.png"></td>
|
||||
<td class=b><img src="../g/rgb24pal.bmp"></td>
|
||||
<td>A 24-bit image, with a palette containing 256 colors. There is little if
|
||||
any reason for a truecolor image to contain a palette, but it is legal.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/rgb24largepal.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="rgb24.png"></td>
|
||||
<td class=b><img src="../q/rgb24largepal.bmp"></td>
|
||||
<td>A 24-bit image, with a palette containing 300 colors.
|
||||
The fact that the palette has more than 256 colors may cause some viewers
|
||||
to complain, but the documentation does not mention a size limit.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/rgb24prof.bmp</td>
|
||||
<td>5</td>
|
||||
<td class=b><img src="rgb24.png"></td>
|
||||
<td class=b><img src="../q/rgb24prof.bmp"></td>
|
||||
<td>My attempt to make a BMP file with an embedded color profile.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/rgb24lprof.bmp</td>
|
||||
<td>5</td>
|
||||
<td class=b><img src="rgb24.png"></td>
|
||||
<td class=b><img src="../q/rgb24lprof.bmp"></td>
|
||||
<td>My attempt to make a BMP file with a linked color profile.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/rgb24jpeg.bmp</td>
|
||||
<td>5</td>
|
||||
<td class=b><img src="rgb24.jpg"></td>
|
||||
<td class=b><img src="../q/rgb24jpeg.bmp"></td>
|
||||
<td rowspan=2>My attempt to make BMP files with embedded JPEG and PNG images.
|
||||
These are not likely to be supported by much of anything (they’re
|
||||
intended for printers).</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/rgb24png.bmp</td>
|
||||
<td>5</td>
|
||||
<td class=b><img src="rgb24.png"></td>
|
||||
<td class=b><img src="../q/rgb24png.bmp"></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/rgb32.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="rgb24.png"></td>
|
||||
<td class=b><img src="../g/rgb32.bmp"></td>
|
||||
<td>A 32-bit image using the default color format for 32-bit images (no
|
||||
BITFIELDS segment). There are 8 bits per color channel, and 8 unused
|
||||
bits. The unused bits are set to 0.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>g/rgb32bf.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="rgb24.png"></td>
|
||||
<td class=b><img src="../g/rgb32bf.bmp"></td>
|
||||
<td>A 32-bit image with a BITFIELDS segment. As usual, there are 8 bits per
|
||||
color channel, and 8 unused bits. But the color channels are in an unusual
|
||||
order, so the viewer must read the BITFIELDS, and not just guess.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/rgb32fakealpha.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="rgb24.png"><br>
|
||||
or<br>
|
||||
<img class=b src="fakealpha.png">
|
||||
</td>
|
||||
<td class=b><img src="../q/rgb32fakealpha.bmp"></td>
|
||||
<td>Same as g/rgb32.bmp, except that the unused bits are set to something
|
||||
other than 0.
|
||||
If the image becomes transparent toward the bottom, it probably means
|
||||
the viewer uses heuristics to guess whether the undefined
|
||||
data represents transparency.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/rgb32-111110.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="rgb24.png"></td>
|
||||
<td class=b><img src="../q/rgb32-111110.bmp"></td>
|
||||
<td>A 32 bits/pixel image, with all 32 bits used: 11 each for red and
|
||||
green, and 10 for blue. As far as I know, this is perfectly valid, but it
|
||||
is unusual.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/rgba32.bmp</td>
|
||||
<td>5</td>
|
||||
<td class=b><img src="rgba32.png"></td>
|
||||
<td class=b><img src="../q/rgba32.bmp"></td>
|
||||
<td>A BMP with an alpha channel. Transparency is barely documented,
|
||||
so it’s <i>possible</i> that this file is not correctly formed.
|
||||
The color channels are in an unusual order, to prevent viewers from
|
||||
passing this test by making a lucky guess.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=q>q/rgba32abf.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b><img src="rgba32.png"></td>
|
||||
<td class=b><img src="../q/rgba32abf.bmp"></td>
|
||||
<td>An image of type BI_ALHPABITFIELDS. Supposedly, this was used on
|
||||
Windows CE. I don’t know whether it is constructed correctly.</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/badbitcount.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/badbitcount.bmp"></td>
|
||||
<td>Header indicates an absurdly large number of bits/pixel.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/badbitssize.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/badbitssize.bmp"></td>
|
||||
<td>Header incorrectly indicates that the bitmap is several GB in size.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/baddens1.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/baddens1.bmp"></td>
|
||||
<td rowspan=2>Density (pixels per meter) suggests the image is <i>much</i>
|
||||
larger in one dimension than the other.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/baddens2.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/baddens2.bmp"></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/badfilesize.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/badfilesize.bmp"></td>
|
||||
<td>Header incorrectly indicates that the file is several GB in size.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/badheadersize.bmp</td>
|
||||
<td>?</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/badheadersize.bmp"></td>
|
||||
<td>Header size is 66 bytes, which is not a valid size for any known BMP
|
||||
version.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/badpalettesize.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/badpalettesize.bmp"></td>
|
||||
<td>Header incorrectly indicates that the palette contains an absurdly large
|
||||
number of colors.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/badplanes.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/badplanes.bmp"></td>
|
||||
<td>The “planes” setting, which is required to be 1, is not 1.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/badrle.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/badrle.bmp"></td>
|
||||
<td>An invalid RLE-compressed image that tries to cause buffer overruns.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/badwidth.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/badwidth.bmp"></td>
|
||||
<td>The image claims to be a negative number of pixels in width.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/pal8badindex.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/pal8badindex.bmp"></td>
|
||||
<td>Many of the palette indices used in the image are not present in the
|
||||
palette.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/reallybig.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/reallybig.bmp"></td>
|
||||
<td>An image with a very large reported width and height.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/rletopdown.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/rletopdown.bmp"></td>
|
||||
<td>An RLE-compressed image that tries to use top-down orientation,
|
||||
which isn’t allowed.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=bad>b/shortfile.bmp</td>
|
||||
<td>3</td>
|
||||
<td class=b>N/A</td>
|
||||
<td class=b><img src="../b/shortfile.bmp"></td>
|
||||
<td>A file that has been truncated in the middle of the bitmap.</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 124 B |
|
Before Width: | Height: | Size: 961 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 83 B After Width: | Height: | Size: 79 B |
BIN
Tests/images/zero_mask_totals.dds
Normal file
|
|
@ -72,7 +72,7 @@ def test_good() -> None:
|
|||
"pal8-0.bmp": "pal8.png",
|
||||
"pal8rle.bmp": "pal8.png",
|
||||
"pal8topdown.bmp": "pal8.png",
|
||||
"pal8nonsquare.bmp": "pal8nonsquare-v.png",
|
||||
"pal8nonsquare.bmp": "pal8nonsquare-e.png",
|
||||
"pal8os2.bmp": "pal8.png",
|
||||
"pal8os2sp.bmp": "pal8.png",
|
||||
"pal8os2v2.bmp": "pal8.png",
|
||||
|
|
@ -95,16 +95,16 @@ def test_good() -> None:
|
|||
for f in get_files("g"):
|
||||
try:
|
||||
with Image.open(f) as im:
|
||||
im.load()
|
||||
with Image.open(get_compare(f)) as compare:
|
||||
compare.load()
|
||||
if im.mode == "P":
|
||||
# assert image similar doesn't really work
|
||||
# with paletized image, since the palette might
|
||||
# be differently ordered for an equivalent image.
|
||||
im = im.convert("RGBA")
|
||||
compare = im.convert("RGBA")
|
||||
assert_image_similar(im, compare, 5)
|
||||
# assert image similar doesn't really work
|
||||
# with paletized image, since the palette might
|
||||
# be differently ordered for an equivalent image.
|
||||
im_converted = im.convert("RGBA") if im.mode == "P" else im
|
||||
compare_converted = (
|
||||
compare.convert("RGBA") if im.mode == "P" else compare
|
||||
)
|
||||
|
||||
assert_image_similar(im_converted, compare_converted, 5)
|
||||
|
||||
except Exception as msg:
|
||||
# there are three here that are unsupported:
|
||||
|
|
|
|||
|
|
@ -28,9 +28,13 @@ def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image:
|
|||
|
||||
|
||||
def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None:
|
||||
it = iter(im.getdata())
|
||||
it = iter(im.get_flattened_data())
|
||||
for data_row in data:
|
||||
im_row = [next(it) for _ in range(im.size[0])]
|
||||
im_row = []
|
||||
for _ in range(im.width):
|
||||
im_v = next(it)
|
||||
assert isinstance(im_v, (int, float))
|
||||
im_row.append(im_v)
|
||||
if any(abs(data_v - im_v) > delta for data_v, im_v in zip(data_row, im_row)):
|
||||
assert im_row == data_row
|
||||
with pytest.raises(StopIteration):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
|
@ -277,25 +278,25 @@ def test_apng_mode() -> None:
|
|||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "P"
|
||||
im.seek(im.n_frames - 1)
|
||||
im = im.convert("RGB")
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0)
|
||||
im_rgb = im.convert("RGB")
|
||||
assert im_rgb.getpixel((0, 0)) == (0, 255, 0)
|
||||
assert im_rgb.getpixel((64, 32)) == (0, 255, 0)
|
||||
|
||||
with Image.open("Tests/images/apng/mode_palette_alpha.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "P"
|
||||
im.seek(im.n_frames - 1)
|
||||
im = im.convert("RGBA")
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
im_rgba = im.convert("RGBA")
|
||||
assert im_rgba.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im_rgba.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "P"
|
||||
im.seek(im.n_frames - 1)
|
||||
im = im.convert("RGBA")
|
||||
assert im.getpixel((0, 0)) == (0, 0, 255, 128)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 255, 128)
|
||||
im_rgba = im.convert("RGBA")
|
||||
assert im_rgba.getpixel((0, 0)) == (0, 0, 255, 128)
|
||||
assert im_rgba.getpixel((64, 32)) == (0, 0, 255, 128)
|
||||
|
||||
|
||||
def test_apng_chunk_errors() -> None:
|
||||
|
|
@ -517,6 +518,24 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
|
|||
assert im.info["duration"] == 600
|
||||
|
||||
|
||||
def test_apng_save_duration_float(tmp_path: Path) -> None:
|
||||
test_file = tmp_path / "temp.png"
|
||||
im = Image.new("1", (1, 1))
|
||||
im2 = Image.new("1", (1, 1), 1)
|
||||
im.save(test_file, save_all=True, append_images=[im2], duration=0.5)
|
||||
|
||||
with Image.open(test_file) as reloaded:
|
||||
assert reloaded.info["duration"] == 0.5
|
||||
|
||||
|
||||
def test_apng_save_large_duration(tmp_path: Path) -> None:
|
||||
test_file = tmp_path / "temp.png"
|
||||
im = Image.new("1", (1, 1))
|
||||
im2 = Image.new("1", (1, 1), 1)
|
||||
with pytest.raises(ValueError, match="cannot write duration"):
|
||||
im.save(test_file, save_all=True, append_images=[im2], duration=65536000)
|
||||
|
||||
|
||||
def test_apng_save_disposal(tmp_path: Path) -> None:
|
||||
test_file = tmp_path / "temp.png"
|
||||
size = (128, 64)
|
||||
|
|
@ -718,6 +737,25 @@ def test_apng_save_size(tmp_path: Path) -> None:
|
|||
assert reloaded.size == (200, 200)
|
||||
|
||||
|
||||
def test_compress_level() -> None:
|
||||
compress_level_sizes = {}
|
||||
for compress_level in (0, 9):
|
||||
out = BytesIO()
|
||||
|
||||
im = Image.new("L", (100, 100))
|
||||
im.save(
|
||||
out,
|
||||
"PNG",
|
||||
save_all=True,
|
||||
append_images=[Image.new("L", (200, 200))],
|
||||
compress_level=compress_level,
|
||||
)
|
||||
|
||||
compress_level_sizes[compress_level] = len(out.getvalue())
|
||||
|
||||
assert compress_level_sizes[0] > compress_level_sizes[9]
|
||||
|
||||
|
||||
def test_seek_after_close() -> None:
|
||||
im = Image.open("Tests/images/apng/delay.png")
|
||||
im.seek(1)
|
||||
|
|
|
|||
|
|
@ -121,7 +121,6 @@ class TestFileAvif:
|
|||
assert image.size == (128, 128)
|
||||
assert image.format == "AVIF"
|
||||
assert image.get_format_mimetype() == "image/avif"
|
||||
image.getdata()
|
||||
|
||||
# generated with:
|
||||
# avifdec hopper.avif hopper_avif_write.png
|
||||
|
|
@ -143,7 +142,6 @@ class TestFileAvif:
|
|||
assert reloaded.mode == "RGB"
|
||||
assert reloaded.size == (128, 128)
|
||||
assert reloaded.format == "AVIF"
|
||||
reloaded.getdata()
|
||||
|
||||
# avifdec hopper.avif avif/hopper_avif_write.png
|
||||
assert_image_similar_tofile(
|
||||
|
|
|
|||
|
|
@ -165,9 +165,9 @@ def test_rgba_bitfields() -> None:
|
|||
with Image.open("Tests/images/rgb32bf-rgba.bmp") as im:
|
||||
# So before the comparing the image, swap the channels
|
||||
b, g, r = im.split()[1:]
|
||||
im = Image.merge("RGB", (r, g, b))
|
||||
im_rgb = Image.merge("RGB", (r, g, b))
|
||||
|
||||
assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
|
||||
assert_image_equal_tofile(im_rgb, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
|
||||
|
||||
# This test image has been manually hexedited
|
||||
# to change the bitfield compression in the header from XBGR to ABGR
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ def test_handler(tmp_path: Path) -> None:
|
|||
|
||||
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
|
||||
self.loaded = True
|
||||
assert im.fp is not None
|
||||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
|
|||
def test_sanity_dxt1_bc1(image_path: str) -> None:
|
||||
"""Check DXT1 and BC1 images can be opened"""
|
||||
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
|
||||
target = target.convert("RGBA")
|
||||
target_rgba = target.convert("RGBA")
|
||||
with Image.open(image_path) as im:
|
||||
im.load()
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ def test_sanity_dxt1_bc1(image_path: str) -> None:
|
|||
assert im.mode == "RGBA"
|
||||
assert im.size == (256, 256)
|
||||
|
||||
assert_image_equal(im, target)
|
||||
assert_image_equal(im, target_rgba)
|
||||
|
||||
|
||||
def test_sanity_dxt3() -> None:
|
||||
|
|
@ -380,6 +380,11 @@ def test_palette() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
|
||||
|
||||
|
||||
def test_zero_mask_totals() -> None:
|
||||
with Image.open("Tests/images/zero_mask_totals.dds") as im:
|
||||
im.load()
|
||||
|
||||
|
||||
def test_unsupported_header_size() -> None:
|
||||
with pytest.raises(OSError, match="Unsupported header size 0"):
|
||||
with Image.open(BytesIO(b"DDS " + b"\x00" * 4)):
|
||||
|
|
@ -515,9 +520,9 @@ def test_save_dx10_bc5(tmp_path: Path) -> None:
|
|||
im.save(out, pixel_format="BC5")
|
||||
assert_image_similar_tofile(im, out, 9.56)
|
||||
|
||||
im = hopper("L")
|
||||
im_l = hopper("L")
|
||||
with pytest.raises(OSError, match="only RGB mode can be written as BC5"):
|
||||
im.save(out, pixel_format="BC5")
|
||||
im_l.save(out, pixel_format="BC5")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
|||
|
|
@ -265,9 +265,9 @@ def test_bytesio_object() -> None:
|
|||
img.load()
|
||||
|
||||
with Image.open(FILE1_COMPARE) as image1_scale1_compare:
|
||||
image1_scale1_compare = image1_scale1_compare.convert("RGB")
|
||||
image1_scale1_compare.load()
|
||||
assert_image_similar(img, image1_scale1_compare, 5)
|
||||
image1_scale1_compare_rgb = image1_scale1_compare.convert("RGB")
|
||||
image1_scale1_compare_rgb.load()
|
||||
assert_image_similar(img, image1_scale1_compare_rgb, 5)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
|
|
@ -301,17 +301,17 @@ def test_render_scale1() -> None:
|
|||
with Image.open(FILE1) as image1_scale1:
|
||||
image1_scale1.load()
|
||||
with Image.open(FILE1_COMPARE) as image1_scale1_compare:
|
||||
image1_scale1_compare = image1_scale1_compare.convert("RGB")
|
||||
image1_scale1_compare.load()
|
||||
assert_image_similar(image1_scale1, image1_scale1_compare, 5)
|
||||
image1_scale1_compare_rgb = image1_scale1_compare.convert("RGB")
|
||||
image1_scale1_compare_rgb.load()
|
||||
assert_image_similar(image1_scale1, image1_scale1_compare_rgb, 5)
|
||||
|
||||
# Non-zero bounding box
|
||||
with Image.open(FILE2) as image2_scale1:
|
||||
image2_scale1.load()
|
||||
with Image.open(FILE2_COMPARE) as image2_scale1_compare:
|
||||
image2_scale1_compare = image2_scale1_compare.convert("RGB")
|
||||
image2_scale1_compare.load()
|
||||
assert_image_similar(image2_scale1, image2_scale1_compare, 10)
|
||||
image2_scale1_compare_rgb = image2_scale1_compare.convert("RGB")
|
||||
image2_scale1_compare_rgb.load()
|
||||
assert_image_similar(image2_scale1, image2_scale1_compare_rgb, 10)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
|
|
@ -324,18 +324,16 @@ def test_render_scale2() -> None:
|
|||
assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile)
|
||||
image1_scale2.load(scale=2)
|
||||
with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare:
|
||||
image1_scale2_compare = image1_scale2_compare.convert("RGB")
|
||||
image1_scale2_compare.load()
|
||||
assert_image_similar(image1_scale2, image1_scale2_compare, 5)
|
||||
image1_scale2_compare_rgb = image1_scale2_compare.convert("RGB")
|
||||
assert_image_similar(image1_scale2, image1_scale2_compare_rgb, 5)
|
||||
|
||||
# Non-zero bounding box
|
||||
with Image.open(FILE2) as image2_scale2:
|
||||
assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile)
|
||||
image2_scale2.load(scale=2)
|
||||
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
|
||||
image2_scale2_compare = image2_scale2_compare.convert("RGB")
|
||||
image2_scale2_compare.load()
|
||||
assert_image_similar(image2_scale2, image2_scale2_compare, 10)
|
||||
image2_scale2_compare_rgb = image2_scale2_compare.convert("RGB")
|
||||
assert_image_similar(image2_scale2, image2_scale2_compare_rgb, 10)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
|
|
@ -345,8 +343,8 @@ def test_render_scale2() -> None:
|
|||
def test_resize(filename: str) -> None:
|
||||
with Image.open(filename) as im:
|
||||
new_size = (100, 100)
|
||||
im = im.resize(new_size)
|
||||
assert im.size == new_size
|
||||
im_resized = im.resize(new_size)
|
||||
assert im_resized.size == new_size
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ def test_multiple_load_operations() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/gbr.png")
|
||||
|
||||
|
||||
def create_gbr_image(info: dict[str, int] = {}, magic_number=b"") -> BytesIO:
|
||||
def create_gbr_image(info: dict[str, int] = {}, magic_number: bytes = b"") -> BytesIO:
|
||||
return BytesIO(
|
||||
b"".join(
|
||||
_binary.o32be(i)
|
||||
|
|
|
|||
|
|
@ -327,14 +327,13 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None:
|
|||
|
||||
im.seek(1)
|
||||
assert im.mode == mode
|
||||
if mode == "RGBA":
|
||||
im = im.convert("RGB")
|
||||
im_rgb = im.convert("RGB") if mode == "RGBA" else im
|
||||
|
||||
# Check a color only from the old palette
|
||||
assert im.getpixel((0, 0)) == original_color
|
||||
assert im_rgb.getpixel((0, 0)) == original_color
|
||||
|
||||
# Check a color from the new palette
|
||||
assert im.getpixel((24, 24)) not in first_frame_colors
|
||||
assert im_rgb.getpixel((24, 24)) not in first_frame_colors
|
||||
|
||||
|
||||
def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None:
|
||||
|
|
@ -354,16 +353,16 @@ def test_palette_handling(tmp_path: Path) -> None:
|
|||
# see https://github.com/python-pillow/Pillow/issues/513
|
||||
|
||||
with Image.open(TEST_GIF) as im:
|
||||
im = im.convert("RGB")
|
||||
im_rgb = im.convert("RGB")
|
||||
|
||||
im = im.resize((100, 100), Image.Resampling.LANCZOS)
|
||||
im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
|
||||
im_rgb = im_rgb.resize((100, 100), Image.Resampling.LANCZOS)
|
||||
im_p = im_rgb.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
|
||||
|
||||
f = tmp_path / "temp.gif"
|
||||
im2.save(f, optimize=True)
|
||||
f = tmp_path / "temp.gif"
|
||||
im_p.save(f, optimize=True)
|
||||
|
||||
with Image.open(f) as reloaded:
|
||||
assert_image_similar(im, reloaded.convert("RGB"), 10)
|
||||
assert_image_similar(im_rgb, reloaded.convert("RGB"), 10)
|
||||
|
||||
|
||||
def test_palette_434(tmp_path: Path) -> None:
|
||||
|
|
@ -383,35 +382,36 @@ def test_palette_434(tmp_path: Path) -> None:
|
|||
with roundtrip(im, optimize=True) as reloaded:
|
||||
assert_image_similar(im, reloaded, 1)
|
||||
|
||||
im = im.convert("RGB")
|
||||
# check automatic P conversion
|
||||
with roundtrip(im) as reloaded:
|
||||
reloaded = reloaded.convert("RGB")
|
||||
assert_image_equal(im, reloaded)
|
||||
im_rgb = im.convert("RGB")
|
||||
|
||||
# check automatic P conversion
|
||||
with roundtrip(im_rgb) as reloaded:
|
||||
reloaded = reloaded.convert("RGB")
|
||||
assert_image_equal(im_rgb, reloaded)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
|
||||
def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
|
||||
with Image.open(TEST_GIF) as img:
|
||||
img = img.convert("RGB")
|
||||
img_rgb = img.convert("RGB")
|
||||
|
||||
tempfile = str(tmp_path / "temp.gif")
|
||||
b = BytesIO()
|
||||
GifImagePlugin._save_netpbm(img, b, tempfile)
|
||||
with Image.open(tempfile) as reloaded:
|
||||
assert_image_similar(img, reloaded.convert("RGB"), 0)
|
||||
tempfile = str(tmp_path / "temp.gif")
|
||||
b = BytesIO()
|
||||
GifImagePlugin._save_netpbm(img_rgb, b, tempfile)
|
||||
with Image.open(tempfile) as reloaded:
|
||||
assert_image_similar(img_rgb, reloaded.convert("RGB"), 0)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
|
||||
def test_save_netpbm_l_mode(tmp_path: Path) -> None:
|
||||
with Image.open(TEST_GIF) as img:
|
||||
img = img.convert("L")
|
||||
img_l = img.convert("L")
|
||||
|
||||
tempfile = str(tmp_path / "temp.gif")
|
||||
b = BytesIO()
|
||||
GifImagePlugin._save_netpbm(img, b, tempfile)
|
||||
GifImagePlugin._save_netpbm(img_l, b, tempfile)
|
||||
with Image.open(tempfile) as reloaded:
|
||||
assert_image_similar(img, reloaded.convert("L"), 0)
|
||||
assert_image_similar(img_l, reloaded.convert("L"), 0)
|
||||
|
||||
|
||||
def test_seek() -> None:
|
||||
|
|
@ -1038,9 +1038,9 @@ def test_webp_background(tmp_path: Path) -> None:
|
|||
im.save(out)
|
||||
|
||||
# Test non-opaque WebP background
|
||||
im = Image.new("L", (100, 100), "#000")
|
||||
im.info["background"] = (0, 0, 0, 0)
|
||||
im.save(out)
|
||||
im2 = Image.new("L", (100, 100), "#000")
|
||||
im2.info["background"] = (0, 0, 0, 0)
|
||||
im2.save(out)
|
||||
|
||||
|
||||
def test_comment(tmp_path: Path) -> None:
|
||||
|
|
@ -1048,16 +1048,16 @@ def test_comment(tmp_path: Path) -> None:
|
|||
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0"
|
||||
|
||||
out = tmp_path / "temp.gif"
|
||||
im = Image.new("L", (100, 100), "#000")
|
||||
im.info["comment"] = b"Test comment text"
|
||||
im.save(out)
|
||||
im2 = Image.new("L", (100, 100), "#000")
|
||||
im2.info["comment"] = b"Test comment text"
|
||||
im2.save(out)
|
||||
with Image.open(out) as reread:
|
||||
assert reread.info["comment"] == im.info["comment"]
|
||||
assert reread.info["comment"] == im2.info["comment"]
|
||||
|
||||
im.info["comment"] = "Test comment text"
|
||||
im.save(out)
|
||||
im2.info["comment"] = "Test comment text"
|
||||
im2.save(out)
|
||||
with Image.open(out) as reread:
|
||||
assert reread.info["comment"] == im.info["comment"].encode()
|
||||
assert reread.info["comment"] == im2.info["comment"].encode()
|
||||
|
||||
# Test that GIF89a is used for comments
|
||||
assert reread.info["version"] == b"GIF89a"
|
||||
|
|
|
|||
|
|
@ -59,8 +59,9 @@ def test_handler(tmp_path: Path) -> None:
|
|||
def open(self, im: Image.Image) -> None:
|
||||
self.opened = True
|
||||
|
||||
def load(self, im: Image.Image) -> Image.Image:
|
||||
def load(self, im: ImageFile.ImageFile) -> Image.Image:
|
||||
self.loaded = True
|
||||
assert im.fp is not None
|
||||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
|
|
|
|||
|
|
@ -61,8 +61,9 @@ def test_handler(tmp_path: Path) -> None:
|
|||
def open(self, im: Image.Image) -> None:
|
||||
self.opened = True
|
||||
|
||||
def load(self, im: Image.Image) -> Image.Image:
|
||||
def load(self, im: ImageFile.ImageFile) -> Image.Image:
|
||||
self.loaded = True
|
||||
assert im.fp is not None
|
||||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import pytest
|
|||
|
||||
from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags
|
||||
|
||||
from .helper import assert_image_equal, hopper
|
||||
from .helper import assert_image_equal
|
||||
|
||||
TEST_FILE = "Tests/images/iptc.jpg"
|
||||
|
||||
|
||||
def create_iptc_image(info: dict[str, int] = {}) -> BytesIO:
|
||||
def field(tag, value):
|
||||
def field(tag: tuple[int, int], value: bytes) -> bytes:
|
||||
return bytes((0x1C,) + tag + (0, len(value))) + value
|
||||
|
||||
data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0))))
|
||||
|
|
@ -85,7 +85,7 @@ def test_getiptcinfo() -> None:
|
|||
|
||||
def test_getiptcinfo_jpg_none() -> None:
|
||||
# Arrange
|
||||
with hopper() as im:
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
# Act
|
||||
iptc = IptcImagePlugin.getiptcinfo(im)
|
||||
|
||||
|
|
@ -143,6 +143,7 @@ def test_getiptcinfo_tiff() -> None:
|
|||
|
||||
# Test with LONG tag type
|
||||
with Image.open("Tests/images/hopper.Lab.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.tag_v2.tagtype[TiffImagePlugin.IPTC_NAA_CHUNK] = TiffTags.LONG
|
||||
iptc = IptcImagePlugin.getiptcinfo(im)
|
||||
|
||||
|
|
|
|||
|
|
@ -1133,8 +1133,9 @@ class TestFileCloseW32:
|
|||
im.save(tmpfile)
|
||||
|
||||
im = Image.open(tmpfile)
|
||||
assert im.fp is not None
|
||||
assert not im.fp.closed
|
||||
fp = im.fp
|
||||
assert not fp.closed
|
||||
with pytest.raises(OSError):
|
||||
os.remove(tmpfile)
|
||||
im.load()
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ def test_reduce() -> None:
|
|||
with Image.open("Tests/images/test-card-lossless.jp2") as im:
|
||||
assert callable(im.reduce)
|
||||
|
||||
im.reduce = 2
|
||||
im.reduce = 2 # type: ignore[assignment, method-assign]
|
||||
assert im.reduce == 2
|
||||
|
||||
im.load()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,15 @@ from typing import Any, NamedTuple
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features
|
||||
from PIL import (
|
||||
Image,
|
||||
ImageFile,
|
||||
ImageFilter,
|
||||
ImageOps,
|
||||
TiffImagePlugin,
|
||||
TiffTags,
|
||||
features,
|
||||
)
|
||||
from PIL.TiffImagePlugin import OSUBFILETYPE, SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
|
||||
|
||||
from .helper import (
|
||||
|
|
@ -27,14 +35,13 @@ from .helper import (
|
|||
|
||||
@skip_unless_feature("libtiff")
|
||||
class LibTiffTestCase:
|
||||
def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None:
|
||||
def _assert_noerr(self, tmp_path: Path, im: ImageFile.ImageFile) -> None:
|
||||
"""Helper tests that assert basic sanity about the g4 tiff reading"""
|
||||
# 1 bit
|
||||
assert im.mode == "1"
|
||||
|
||||
# Does the data actually load
|
||||
im.load()
|
||||
im.getdata()
|
||||
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im._compression == "group4"
|
||||
|
|
@ -355,6 +362,36 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Should not segfault
|
||||
im.save(outfile)
|
||||
|
||||
@pytest.mark.parametrize("tagtype", (TiffTags.SIGNED_RATIONAL, TiffTags.IFD))
|
||||
def test_tag_type(
|
||||
self, tagtype: int, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
ifd[37000] = 100
|
||||
ifd.tagtype[37000] = tagtype
|
||||
|
||||
out = tmp_path / "temp.tif"
|
||||
im = Image.new("L", (1, 1))
|
||||
im.save(out, tiffinfo=ifd)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[37000] == 100
|
||||
|
||||
def test_inknames_tag(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
|
||||
out = tmp_path / "temp.tif"
|
||||
hopper("L").save(out, tiffinfo={333: "name\x00"})
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[333] in ("name", "name\x00")
|
||||
|
||||
def test_whitepoint_tag(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
|
|
@ -478,12 +515,12 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# and save to compressed tif.
|
||||
out = tmp_path / "temp.tif"
|
||||
with Image.open("Tests/images/pport_g4.tif") as im:
|
||||
im = im.convert("L")
|
||||
im_l = im.convert("L")
|
||||
|
||||
im = im.filter(ImageFilter.GaussianBlur(4))
|
||||
im.save(out, compression="tiff_adobe_deflate")
|
||||
im_l = im_l.filter(ImageFilter.GaussianBlur(4))
|
||||
im_l.save(out, compression="tiff_adobe_deflate")
|
||||
|
||||
assert_image_equal_tofile(im, out)
|
||||
assert_image_equal_tofile(im_l, out)
|
||||
|
||||
def test_compressions(self, tmp_path: Path) -> None:
|
||||
# Test various tiff compressions and assert similar image content but reduced
|
||||
|
|
@ -572,8 +609,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im.save(out, compression=compression)
|
||||
|
||||
def test_fp_leak(self) -> None:
|
||||
im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif")
|
||||
im: ImageFile.ImageFile | None = Image.open("Tests/images/hopper_g4_500.tif")
|
||||
assert im is not None
|
||||
assert im.fp is not None
|
||||
fn = im.fp.fileno()
|
||||
|
||||
os.fstat(fn)
|
||||
|
|
@ -1049,8 +1087,10 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
data = data[:102] + b"\x02" + data[103:]
|
||||
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
||||
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
|
||||
im_transposed = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
||||
assert_image_equal_tofile(
|
||||
im_transposed, "Tests/images/old-style-jpeg-compression.png"
|
||||
)
|
||||
|
||||
def test_open_missing_samplesperpixel(self) -> None:
|
||||
with Image.open(
|
||||
|
|
@ -1117,9 +1157,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
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)
|
||||
im_transposed = ImageOps.exif_transpose(im)
|
||||
|
||||
assert_image_similar(base_im, im, 0.7)
|
||||
assert_image_similar(base_im, im_transposed, 0.7)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ def test_sanity() -> None:
|
|||
|
||||
# Adjust for the gamma of 2.2 encoded into the file
|
||||
lut = ImagePalette.make_gamma_lut(1 / 2.2)
|
||||
im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()])
|
||||
im1 = Image.merge("RGBA", [chan.point(lut) for chan in im.split()])
|
||||
|
||||
im2 = hopper("RGBA")
|
||||
assert_image_similar(im, im2, 10)
|
||||
assert_image_similar(im1, im2, 10)
|
||||
|
||||
|
||||
def test_n_frames() -> None:
|
||||
|
|
|
|||
|
|
@ -300,12 +300,12 @@ def test_save_all() -> None:
|
|||
im_reloaded.seek(1)
|
||||
assert_image_similar(im, im_reloaded, 30)
|
||||
|
||||
im = Image.new("RGB", (1, 1))
|
||||
im_rgb = Image.new("RGB", (1, 1))
|
||||
for colors in (("#f00",), ("#f00", "#0f0")):
|
||||
append_images = [Image.new("RGB", (1, 1), color) for color in colors]
|
||||
im_reloaded = roundtrip(im, save_all=True, append_images=append_images)
|
||||
im_reloaded = roundtrip(im_rgb, save_all=True, append_images=append_images)
|
||||
|
||||
assert_image_equal(im, im_reloaded)
|
||||
assert_image_equal(im_rgb, im_reloaded)
|
||||
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
|
||||
assert im_reloaded.mpinfo is not None
|
||||
assert im_reloaded.mpinfo[45056] == b"0100"
|
||||
|
|
@ -315,7 +315,7 @@ def test_save_all() -> None:
|
|||
assert_image_similar(im_reloaded, im_expected, 1)
|
||||
|
||||
# Test that a single frame image will not be saved as an MPO
|
||||
jpg = roundtrip(im, save_all=True)
|
||||
jpg = roundtrip(im_rgb, save_all=True)
|
||||
assert "mp" not in jpg.info
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -101,12 +101,13 @@ class TestFilePng:
|
|||
assert im.get_format_mimetype() == "image/png"
|
||||
|
||||
for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]:
|
||||
im = hopper(mode)
|
||||
im.save(test_file)
|
||||
im1 = hopper(mode)
|
||||
im1.save(test_file)
|
||||
with Image.open(test_file) as reloaded:
|
||||
if mode == "I;16B":
|
||||
reloaded = reloaded.convert(mode)
|
||||
assert_image_equal(reloaded, im)
|
||||
converted_reloaded = (
|
||||
reloaded.convert(mode) if mode == "I;16B" else reloaded
|
||||
)
|
||||
assert_image_equal(converted_reloaded, im1)
|
||||
|
||||
def test_invalid_file(self) -> None:
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
|
@ -225,11 +226,11 @@ class TestFilePng:
|
|||
test_file = "Tests/images/pil123p.png"
|
||||
with Image.open(test_file) as im:
|
||||
assert_image(im, "P", (162, 150))
|
||||
im = im.convert("RGBA")
|
||||
assert_image(im, "RGBA", (162, 150))
|
||||
im_rgba = im.convert("RGBA")
|
||||
assert_image(im_rgba, "RGBA", (162, 150))
|
||||
|
||||
# image has 124 unique alpha values
|
||||
colors = im.getchannel("A").getcolors()
|
||||
colors = im_rgba.getchannel("A").getcolors()
|
||||
assert colors is not None
|
||||
assert len(colors) == 124
|
||||
|
||||
|
|
@ -239,11 +240,11 @@ class TestFilePng:
|
|||
assert im.info["transparency"] == (0, 255, 52)
|
||||
|
||||
assert_image(im, "RGB", (64, 64))
|
||||
im = im.convert("RGBA")
|
||||
assert_image(im, "RGBA", (64, 64))
|
||||
im_rgba = im.convert("RGBA")
|
||||
assert_image(im_rgba, "RGBA", (64, 64))
|
||||
|
||||
# image has 876 transparent pixels
|
||||
colors = im.getchannel("A").getcolors()
|
||||
colors = im_rgba.getchannel("A").getcolors()
|
||||
assert colors is not None
|
||||
assert colors[0][0] == 876
|
||||
|
||||
|
|
@ -262,11 +263,11 @@ class TestFilePng:
|
|||
assert len(im.info["transparency"]) == 256
|
||||
|
||||
assert_image(im, "P", (162, 150))
|
||||
im = im.convert("RGBA")
|
||||
assert_image(im, "RGBA", (162, 150))
|
||||
im_rgba = im.convert("RGBA")
|
||||
assert_image(im_rgba, "RGBA", (162, 150))
|
||||
|
||||
# image has 124 unique alpha values
|
||||
colors = im.getchannel("A").getcolors()
|
||||
colors = im_rgba.getchannel("A").getcolors()
|
||||
assert colors is not None
|
||||
assert len(colors) == 124
|
||||
|
||||
|
|
@ -285,13 +286,13 @@ class TestFilePng:
|
|||
assert im.info["transparency"] == 164
|
||||
assert im.getpixel((31, 31)) == 164
|
||||
assert_image(im, "P", (64, 64))
|
||||
im = im.convert("RGBA")
|
||||
assert_image(im, "RGBA", (64, 64))
|
||||
im_rgba = im.convert("RGBA")
|
||||
assert_image(im_rgba, "RGBA", (64, 64))
|
||||
|
||||
assert im.getpixel((31, 31)) == (0, 255, 52, 0)
|
||||
assert im_rgba.getpixel((31, 31)) == (0, 255, 52, 0)
|
||||
|
||||
# image has 876 transparent pixels
|
||||
colors = im.getchannel("A").getcolors()
|
||||
colors = im_rgba.getchannel("A").getcolors()
|
||||
assert colors is not None
|
||||
assert colors[0][0] == 876
|
||||
|
||||
|
|
@ -338,6 +339,15 @@ class TestFilePng:
|
|||
assert colors is not None
|
||||
assert colors[0][0] == num_transparent
|
||||
|
||||
def test_save_1_transparency(self, tmp_path: Path) -> None:
|
||||
out = tmp_path / "temp.png"
|
||||
|
||||
im = Image.new("1", (1, 1), 1)
|
||||
im.save(out, transparency=1)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert reloaded.info["transparency"] == 255
|
||||
|
||||
def test_save_rgb_single_transparency(self, tmp_path: Path) -> None:
|
||||
in_file = "Tests/images/caption_6_33_22.png"
|
||||
with Image.open(in_file) as im:
|
||||
|
|
@ -778,7 +788,9 @@ class TestFilePng:
|
|||
im.save(test_file, exif=im.getexif())
|
||||
|
||||
with Image.open(test_file) as reloaded:
|
||||
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
|
||||
exif = reloaded._getexif()
|
||||
assert exif is not None
|
||||
assert exif[305] == "Adobe Photoshop CS Macintosh"
|
||||
|
||||
def test_exif_argument(self, tmp_path: Path) -> None:
|
||||
|
|
@ -811,7 +823,7 @@ class TestFilePng:
|
|||
monkeypatch.setattr(sys, "stdout", mystdout)
|
||||
|
||||
with Image.open(TEST_PNG_FILE) as im:
|
||||
im.save(sys.stdout, "PNG")
|
||||
im.save(sys.stdout, "PNG") # type: ignore[arg-type]
|
||||
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
|
|
|
|||
|
|
@ -389,7 +389,7 @@ def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
monkeypatch.setattr(sys, "stdout", mystdout)
|
||||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.save(sys.stdout, "PPM")
|
||||
im.save(sys.stdout, "PPM") # type: ignore[arg-type]
|
||||
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ def test_seek_tell() -> None:
|
|||
|
||||
im.seek(2)
|
||||
layer_number = im.tell()
|
||||
assert layer_number == 2
|
||||
assert layer_number == 2
|
||||
|
||||
|
||||
def test_seek_eoferror() -> None:
|
||||
|
|
@ -138,7 +138,7 @@ def test_icc_profile() -> None:
|
|||
assert "icc_profile" in im.info
|
||||
|
||||
icc_profile = im.info["icc_profile"]
|
||||
assert len(icc_profile) == 3144
|
||||
assert len(icc_profile) == 3144
|
||||
|
||||
|
||||
def test_no_icc_profile() -> None:
|
||||
|
|
@ -158,17 +158,16 @@ def test_combined_larger_than_size() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file,raises",
|
||||
"test_file",
|
||||
[
|
||||
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
|
||||
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
||||
"Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd",
|
||||
"Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd",
|
||||
],
|
||||
)
|
||||
def test_crashes(test_file: str, raises: type[Exception]) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with pytest.raises(raises):
|
||||
with Image.open(f):
|
||||
pass
|
||||
def test_crashes(test_file: str) -> None:
|
||||
with pytest.raises(OSError):
|
||||
with Image.open(test_file):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -179,8 +178,7 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None:
|
|||
],
|
||||
)
|
||||
def test_layer_crashes(test_file: str) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with Image.open(f) as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
with pytest.raises(SyntaxError):
|
||||
im.layers
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
with pytest.raises(SyntaxError):
|
||||
im.layers
|
||||
|
|
|
|||
|
|
@ -84,8 +84,8 @@ def test_rgbx() -> None:
|
|||
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
r, g, b = im.split()
|
||||
im = Image.merge("RGB", (b, g, r))
|
||||
assert_image_equal_tofile(im, os.path.join(EXTRA_DIR, "32bpp.png"))
|
||||
im_rgb = Image.merge("RGB", (b, g, r))
|
||||
assert_image_equal_tofile(im_rgb, os.path.join(EXTRA_DIR, "32bpp.png"))
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
|
|
|
|||
|
|
@ -764,9 +764,9 @@ class TestFileTiff:
|
|||
|
||||
# Test appending images
|
||||
mp = BytesIO()
|
||||
im = Image.new("RGB", (100, 100), "#f00")
|
||||
im_rgb = Image.new("RGB", (100, 100), "#f00")
|
||||
ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]]
|
||||
im.copy().save(mp, format="TIFF", save_all=True, append_images=ims)
|
||||
im_rgb.copy().save(mp, format="TIFF", save_all=True, append_images=ims)
|
||||
|
||||
mp.seek(0, os.SEEK_SET)
|
||||
with Image.open(mp) as reread:
|
||||
|
|
@ -778,7 +778,7 @@ class TestFileTiff:
|
|||
yield from ims
|
||||
|
||||
mp = BytesIO()
|
||||
im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
|
||||
im_rgb.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
|
||||
|
||||
mp.seek(0, os.SEEK_SET)
|
||||
with Image.open(mp) as reread:
|
||||
|
|
@ -971,6 +971,7 @@ class TestFileTiff:
|
|||
|
||||
im = Image.open(tmpfile)
|
||||
fp = im.fp
|
||||
assert fp is not None
|
||||
assert not fp.closed
|
||||
im.load()
|
||||
assert fp.closed
|
||||
|
|
@ -984,6 +985,7 @@ class TestFileTiff:
|
|||
with open(tmpfile, "rb") as f:
|
||||
im = Image.open(f)
|
||||
fp = im.fp
|
||||
assert fp is not None
|
||||
assert not fp.closed
|
||||
im.load()
|
||||
assert not fp.closed
|
||||
|
|
@ -1034,8 +1036,9 @@ class TestFileTiffW32:
|
|||
im.save(tmpfile)
|
||||
|
||||
im = Image.open(tmpfile)
|
||||
assert im.fp is not None
|
||||
assert not im.fp.closed
|
||||
fp = im.fp
|
||||
assert not fp.closed
|
||||
with pytest.raises(OSError):
|
||||
os.remove(tmpfile)
|
||||
im.load()
|
||||
|
|
|
|||
|
|
@ -175,13 +175,13 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
|
|||
del info[278]
|
||||
|
||||
# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
|
||||
im = im.resize((500, 500))
|
||||
info[TiffImagePlugin.IMAGEWIDTH] = im.width
|
||||
im_resized = im.resize((500, 500))
|
||||
info[TiffImagePlugin.IMAGEWIDTH] = im_resized.width
|
||||
|
||||
# STRIPBYTECOUNTS can be a SHORT or a LONG
|
||||
info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT
|
||||
|
||||
im.save(out, tiffinfo=info)
|
||||
im_resized.save(out, tiffinfo=info)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ class TestFileWebp:
|
|||
assert image.size == (128, 128)
|
||||
assert image.format == "WEBP"
|
||||
image.load()
|
||||
image.getdata()
|
||||
|
||||
# generated with:
|
||||
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
|
||||
|
|
@ -77,7 +76,6 @@ class TestFileWebp:
|
|||
assert image.size == (128, 128)
|
||||
assert image.format == "WEBP"
|
||||
image.load()
|
||||
image.getdata()
|
||||
|
||||
if mode == self.rgb_mode:
|
||||
# generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ def test_read_rgba() -> None:
|
|||
assert image.size == (200, 150)
|
||||
assert image.format == "WEBP"
|
||||
image.load()
|
||||
image.getdata()
|
||||
|
||||
image.tobytes()
|
||||
|
||||
|
|
@ -60,7 +59,6 @@ def test_write_lossless_rgb(tmp_path: Path) -> None:
|
|||
assert image.size == pil_image.size
|
||||
assert image.format == "WEBP"
|
||||
image.load()
|
||||
image.getdata()
|
||||
|
||||
assert_image_equal(image, pil_image)
|
||||
|
||||
|
|
@ -83,7 +81,6 @@ def test_write_rgba(tmp_path: Path) -> None:
|
|||
assert image.size == (10, 10)
|
||||
assert image.format == "WEBP"
|
||||
image.load()
|
||||
image.getdata()
|
||||
|
||||
assert_image_similar(image, pil_image, 1.0)
|
||||
|
||||
|
|
@ -133,7 +130,6 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
|
|||
assert image.format == "WEBP"
|
||||
|
||||
image.load()
|
||||
image.getdata()
|
||||
with Image.open(file_path) as im:
|
||||
target = im.convert("RGBA")
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,5 @@ def test_write_lossless_rgb(tmp_path: Path) -> None:
|
|||
assert image.size == (128, 128)
|
||||
assert image.format == "WEBP"
|
||||
image.load()
|
||||
image.getdata()
|
||||
|
||||
assert_image_equal(image, hopper(RGB_MODE))
|
||||
|
|
|
|||
|
|
@ -13,15 +13,15 @@ def test_white() -> None:
|
|||
|
||||
k = i.getpixel((0, 0))
|
||||
|
||||
L = i.getdata(0)
|
||||
a = i.getdata(1)
|
||||
b = i.getdata(2)
|
||||
L = i.get_flattened_data(0)
|
||||
a = i.get_flattened_data(1)
|
||||
b = i.get_flattened_data(2)
|
||||
|
||||
assert k == (255, 128, 128)
|
||||
|
||||
assert list(L) == [255] * 100
|
||||
assert list(a) == [128] * 100
|
||||
assert list(b) == [128] * 100
|
||||
assert L == (255,) * 100
|
||||
assert a == (128,) * 100
|
||||
assert b == (128,) * 100
|
||||
|
||||
|
||||
def test_green() -> None:
|
||||
|
|
|
|||
|
|
@ -613,8 +613,8 @@ class TestImage:
|
|||
assert im.getpixel((0, 0)) == 0
|
||||
assert im.getpixel((255, 255)) == 255
|
||||
with Image.open(target_file) as target:
|
||||
target = target.convert(mode)
|
||||
assert_image_equal(im, target)
|
||||
im_target = target.convert(mode)
|
||||
assert_image_equal(im, im_target)
|
||||
|
||||
def test_radial_gradient_wrong_mode(self) -> None:
|
||||
# Arrange
|
||||
|
|
@ -638,8 +638,8 @@ class TestImage:
|
|||
assert im.getpixel((0, 0)) == 255
|
||||
assert im.getpixel((128, 128)) == 0
|
||||
with Image.open(target_file) as target:
|
||||
target = target.convert(mode)
|
||||
assert_image_equal(im, target)
|
||||
im_target = target.convert(mode)
|
||||
assert_image_equal(im, im_target)
|
||||
|
||||
def test_register_extensions(self) -> None:
|
||||
test_format = "a"
|
||||
|
|
@ -663,20 +663,20 @@ class TestImage:
|
|||
assert_image_equal(im, im.remap_palette(list(range(256))))
|
||||
|
||||
# Test identity transform with an RGBA palette
|
||||
im = Image.new("P", (256, 1))
|
||||
im_p = Image.new("P", (256, 1))
|
||||
for x in range(256):
|
||||
im.putpixel((x, 0), x)
|
||||
im.putpalette(list(range(256)) * 4, "RGBA")
|
||||
im_remapped = im.remap_palette(list(range(256)))
|
||||
assert_image_equal(im, im_remapped)
|
||||
assert im.palette is not None
|
||||
im_p.putpixel((x, 0), x)
|
||||
im_p.putpalette(list(range(256)) * 4, "RGBA")
|
||||
im_remapped = im_p.remap_palette(list(range(256)))
|
||||
assert_image_equal(im_p, im_remapped)
|
||||
assert im_p.palette is not None
|
||||
assert im_remapped.palette is not None
|
||||
assert im.palette.palette == im_remapped.palette.palette
|
||||
assert im_p.palette.palette == im_remapped.palette.palette
|
||||
|
||||
# Test illegal image mode
|
||||
with hopper() as im:
|
||||
with hopper() as im_hopper:
|
||||
with pytest.raises(ValueError):
|
||||
im.remap_palette([])
|
||||
im_hopper.remap_palette([])
|
||||
|
||||
def test_remap_palette_transparency(self) -> None:
|
||||
im = Image.new("P", (1, 2), (0, 0, 0))
|
||||
|
|
@ -1181,10 +1181,10 @@ class TestImageBytes:
|
|||
assert reloaded.tobytes() == source_bytes
|
||||
|
||||
@pytest.mark.parametrize("mode", Image.MODES)
|
||||
def test_getdata_putdata(self, mode: str) -> None:
|
||||
def test_get_flattened_data_putdata(self, mode: str) -> None:
|
||||
im = hopper(mode)
|
||||
reloaded = Image.new(mode, im.size)
|
||||
reloaded.putdata(im.getdata())
|
||||
reloaded.putdata(im.get_flattened_data())
|
||||
assert_image_equal(im, reloaded)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ def test_fromarray() -> None:
|
|||
},
|
||||
)
|
||||
out = Image.fromarray(wrapped)
|
||||
return out.mode, out.size, list(i.getdata()) == list(out.getdata())
|
||||
return out.mode, out.size, i.get_flattened_data() == out.get_flattened_data()
|
||||
|
||||
# assert test("1") == ("1", (128, 100), True)
|
||||
assert test("L") == ("L", (128, 100), True)
|
||||
|
|
|
|||
|
|
@ -80,8 +80,8 @@ def test_16bit() -> None:
|
|||
_test_float_conversion(im)
|
||||
|
||||
for color in (65535, 65536):
|
||||
im = Image.new("I", (1, 1), color)
|
||||
im_i16 = im.convert("I;16")
|
||||
im_i = Image.new("I", (1, 1), color)
|
||||
im_i16 = im_i.convert("I;16")
|
||||
assert im_i16.getpixel((0, 0)) == 65535
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -78,13 +78,13 @@ def test_crop_crash() -> None:
|
|||
extents = (1, 1, 10, 10)
|
||||
# works prepatch
|
||||
with Image.open(test_img) as img:
|
||||
img2 = img.crop(extents)
|
||||
img2.load()
|
||||
img1 = img.crop(extents)
|
||||
img1.load()
|
||||
|
||||
# fail prepatch
|
||||
with Image.open(test_img) as img:
|
||||
img = img.crop(extents)
|
||||
img.load()
|
||||
img2 = img.crop(extents)
|
||||
img2.load()
|
||||
|
||||
|
||||
def test_crop_zero() -> None:
|
||||
|
|
@ -95,10 +95,10 @@ def test_crop_zero() -> None:
|
|||
|
||||
cropped = im.crop((10, 10, 20, 20))
|
||||
assert cropped.size == (10, 10)
|
||||
assert cropped.getdata()[0] == (0, 0, 0)
|
||||
assert cropped.getpixel((0, 0)) == (0, 0, 0)
|
||||
|
||||
im = Image.new("RGB", (0, 0))
|
||||
|
||||
cropped = im.crop((10, 10, 20, 20))
|
||||
assert cropped.size == (10, 10)
|
||||
assert cropped.getdata()[2] == (0, 0, 0)
|
||||
assert cropped.getpixel((2, 0)) == (0, 0, 0)
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import hopper
|
||||
|
||||
|
||||
def test_sanity() -> None:
|
||||
data = hopper().getdata()
|
||||
|
||||
len(data)
|
||||
list(data)
|
||||
data = hopper().get_flattened_data()
|
||||
|
||||
assert len(data) == 128 * 128
|
||||
assert data[0] == (20, 20, 70)
|
||||
|
||||
|
||||
def test_mode() -> None:
|
||||
def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]:
|
||||
def getdata(mode: str) -> tuple[float | tuple[int, ...] | None, int, int]:
|
||||
im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST)
|
||||
data = im.getdata()
|
||||
data = im.get_flattened_data()
|
||||
return data[0], len(data), len(list(data))
|
||||
|
||||
assert getdata("1") == (0, 960, 960)
|
||||
|
|
@ -28,3 +28,13 @@ def test_mode() -> None:
|
|||
assert getdata("RGBA") == ((11, 13, 52, 255), 960, 960)
|
||||
assert getdata("CMYK") == ((244, 242, 203, 0), 960, 960)
|
||||
assert getdata("YCbCr") == ((16, 147, 123), 960, 960)
|
||||
|
||||
|
||||
def test_deprecation() -> None:
|
||||
im = hopper()
|
||||
with pytest.warns(DeprecationWarning, match="getdata"):
|
||||
data = im.getdata()
|
||||
|
||||
assert len(data) == 128 * 128
|
||||
assert data[0] == (20, 20, 70)
|
||||
assert list(data)[0] == (20, 20, 70)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None:
|
|||
def test_contextmanager() -> None:
|
||||
fn = None
|
||||
with Image.open("Tests/images/hopper.gif") as im:
|
||||
assert im.fp is not None
|
||||
fn = im.fp.fileno()
|
||||
os.fstat(fn)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import sys
|
||||
from array import array
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -12,21 +13,19 @@ from .helper import assert_image_equal, hopper
|
|||
|
||||
def test_sanity() -> None:
|
||||
im1 = hopper()
|
||||
for data in (im1.get_flattened_data(), im1.im):
|
||||
im2 = Image.new(im1.mode, im1.size, 0)
|
||||
im2.putdata(data)
|
||||
|
||||
data = list(im1.getdata())
|
||||
assert_image_equal(im1, im2)
|
||||
|
||||
im2 = Image.new(im1.mode, im1.size, 0)
|
||||
im2.putdata(data)
|
||||
# readonly
|
||||
im2 = Image.new(im1.mode, im2.size, 0)
|
||||
im2.readonly = 1
|
||||
im2.putdata(data)
|
||||
|
||||
assert_image_equal(im1, im2)
|
||||
|
||||
# readonly
|
||||
im2 = Image.new(im1.mode, im2.size, 0)
|
||||
im2.readonly = 1
|
||||
im2.putdata(data)
|
||||
|
||||
assert not im2.readonly
|
||||
assert_image_equal(im1, im2)
|
||||
assert not im2.readonly
|
||||
assert_image_equal(im1, im2)
|
||||
|
||||
|
||||
def test_long_integers() -> None:
|
||||
|
|
@ -60,22 +59,22 @@ def test_mode_with_L_with_float() -> None:
|
|||
@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B"))
|
||||
def test_mode_i(mode: str) -> None:
|
||||
src = hopper("L")
|
||||
data = list(src.getdata())
|
||||
data = src.get_flattened_data()
|
||||
im = Image.new(mode, src.size, 0)
|
||||
im.putdata(data, 2, 256)
|
||||
|
||||
target = [2 * elt + 256 for elt in data]
|
||||
assert list(im.getdata()) == target
|
||||
target = tuple(2 * elt + 256 for elt in cast(tuple[int, ...], data))
|
||||
assert im.get_flattened_data() == target
|
||||
|
||||
|
||||
def test_mode_F() -> None:
|
||||
src = hopper("L")
|
||||
data = list(src.getdata())
|
||||
data = src.get_flattened_data()
|
||||
im = Image.new("F", src.size, 0)
|
||||
im.putdata(data, 2.0, 256.0)
|
||||
|
||||
target = [2.0 * float(elt) + 256.0 for elt in data]
|
||||
assert list(im.getdata()) == target
|
||||
target = tuple(2.0 * float(elt) + 256.0 for elt in cast(tuple[int, ...], data))
|
||||
assert im.get_flattened_data() == target
|
||||
|
||||
|
||||
def test_array_B() -> None:
|
||||
|
|
@ -86,7 +85,7 @@ def test_array_B() -> None:
|
|||
im = Image.new("L", (150, 100))
|
||||
im.putdata(arr)
|
||||
|
||||
assert len(im.getdata()) == len(arr)
|
||||
assert len(im.get_flattened_data()) == len(arr)
|
||||
|
||||
|
||||
def test_array_F() -> None:
|
||||
|
|
@ -97,7 +96,7 @@ def test_array_F() -> None:
|
|||
arr = array("f", [0.0]) * 15000
|
||||
im.putdata(arr)
|
||||
|
||||
assert len(im.getdata()) == len(arr)
|
||||
assert len(im.get_flattened_data()) == len(arr)
|
||||
|
||||
|
||||
def test_not_flattened() -> None:
|
||||
|
|
|
|||
|
|
@ -58,8 +58,8 @@ def test_rgba_quantize() -> None:
|
|||
|
||||
def test_quantize() -> None:
|
||||
with Image.open("Tests/images/caption_6_33_22.png") as image:
|
||||
image = image.convert("RGB")
|
||||
converted = image.quantize()
|
||||
converted = image.convert("RGB")
|
||||
converted = converted.quantize()
|
||||
assert converted.mode == "P"
|
||||
assert_image_similar(converted.convert("RGB"), image, 1)
|
||||
|
||||
|
|
@ -67,13 +67,13 @@ def test_quantize() -> None:
|
|||
def test_quantize_no_dither() -> None:
|
||||
image = hopper()
|
||||
with Image.open("Tests/images/caption_6_33_22.png") as palette:
|
||||
palette = palette.convert("P")
|
||||
palette_p = palette.convert("P")
|
||||
|
||||
converted = image.quantize(dither=Image.Dither.NONE, palette=palette)
|
||||
converted = image.quantize(dither=Image.Dither.NONE, palette=palette_p)
|
||||
assert converted.mode == "P"
|
||||
assert converted.palette is not None
|
||||
assert palette.palette is not None
|
||||
assert converted.palette.palette == palette.palette.palette
|
||||
assert palette_p.palette is not None
|
||||
assert converted.palette.palette == palette_p.palette.palette
|
||||
|
||||
|
||||
def test_quantize_no_dither2() -> None:
|
||||
|
|
@ -97,10 +97,10 @@ def test_quantize_no_dither2() -> None:
|
|||
def test_quantize_dither_diff() -> None:
|
||||
image = hopper()
|
||||
with Image.open("Tests/images/caption_6_33_22.png") as palette:
|
||||
palette = palette.convert("P")
|
||||
palette_p = palette.convert("P")
|
||||
|
||||
dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette)
|
||||
nodither = image.quantize(dither=Image.Dither.NONE, palette=palette)
|
||||
dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette_p)
|
||||
nodither = image.quantize(dither=Image.Dither.NONE, palette=palette_p)
|
||||
|
||||
assert dither.tobytes() != nodither.tobytes()
|
||||
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ class TestImagingCoreResize:
|
|||
r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample)
|
||||
assert r.mode == "RGB"
|
||||
assert r.size == (212, 195)
|
||||
assert r.getdata()[0] == (0, 0, 0)
|
||||
assert r.getpixel((0, 0)) == (0, 0, 0)
|
||||
|
||||
def test_unknown_filter(self) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
|
|
@ -314,8 +314,8 @@ class TestImageResize:
|
|||
@skip_unless_feature("libtiff")
|
||||
def test_transposed(self) -> None:
|
||||
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||
im = im.resize((64, 64))
|
||||
assert im.size == (64, 64)
|
||||
im_resized = im.resize((64, 64))
|
||||
assert im_resized.size == (64, 64)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode", ("L", "RGB", "I", "I;16", "I;16L", "I;16B", "I;16N", "F")
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ def test_angle(angle: int) -> None:
|
|||
with Image.open("Tests/images/test-card.png") as im:
|
||||
rotate(im, im.mode, angle)
|
||||
|
||||
im = hopper()
|
||||
assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
|
||||
im_hopper = hopper()
|
||||
assert_image_equal(im_hopper.rotate(angle), im_hopper.rotate(angle, expand=1))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270))
|
||||
|
|
@ -76,9 +76,9 @@ def test_center_0() -> None:
|
|||
|
||||
with Image.open("Tests/images/hopper_45.png") as target:
|
||||
target_origin = target.size[1] / 2
|
||||
target = target.crop((0, target_origin, 128, target_origin + 128))
|
||||
im_target = target.crop((0, target_origin, 128, target_origin + 128))
|
||||
|
||||
assert_image_similar(im, target, 15)
|
||||
assert_image_similar(im, im_target, 15)
|
||||
|
||||
|
||||
def test_center_14() -> None:
|
||||
|
|
@ -87,22 +87,22 @@ def test_center_14() -> None:
|
|||
|
||||
with Image.open("Tests/images/hopper_45.png") as target:
|
||||
target_origin = target.size[1] / 2 - 14
|
||||
target = target.crop((6, target_origin, 128 + 6, target_origin + 128))
|
||||
im_target = target.crop((6, target_origin, 128 + 6, target_origin + 128))
|
||||
|
||||
assert_image_similar(im, target, 10)
|
||||
assert_image_similar(im, im_target, 10)
|
||||
|
||||
|
||||
def test_translate() -> None:
|
||||
im = hopper()
|
||||
with Image.open("Tests/images/hopper_45.png") as target:
|
||||
target_origin = (target.size[1] / 2 - 64) - 5
|
||||
target = target.crop(
|
||||
im_target = target.crop(
|
||||
(target_origin, target_origin, target_origin + 128, target_origin + 128)
|
||||
)
|
||||
|
||||
im = im.rotate(45, translate=(5, 5), resample=Image.Resampling.BICUBIC)
|
||||
|
||||
assert_image_similar(im, target, 1)
|
||||
assert_image_similar(im, im_target, 1)
|
||||
|
||||
|
||||
def test_fastpath_center() -> None:
|
||||
|
|
|
|||
|
|
@ -159,9 +159,9 @@ def test_reducing_gap_for_DCT_scaling() -> None:
|
|||
with Image.open("Tests/images/hopper.jpg") as ref:
|
||||
# thumbnail should call draft with reducing_gap scale
|
||||
ref.draft(None, (18 * 3, 18 * 3))
|
||||
ref = ref.resize((18, 18), Image.Resampling.BICUBIC)
|
||||
im_ref = ref.resize((18, 18), Image.Resampling.BICUBIC)
|
||||
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
|
||||
|
||||
assert_image_similar(ref, im, 1.4)
|
||||
assert_image_similar(im_ref, im, 1.4)
|
||||
|
|
|
|||
|
|
@ -250,14 +250,14 @@ class TestImageTransform:
|
|||
def test_missing_method_data(self) -> None:
|
||||
with hopper() as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.transform((100, 100), None)
|
||||
im.transform((100, 100), None) # type: ignore[arg-type]
|
||||
|
||||
@pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown"))
|
||||
def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None:
|
||||
with hopper() as im:
|
||||
(w, h) = im.size
|
||||
with pytest.raises(ValueError):
|
||||
im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample)
|
||||
im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) # type: ignore[arg-type]
|
||||
|
||||
|
||||
class TestImageTransformAffine:
|
||||
|
|
|
|||
|
|
@ -274,13 +274,13 @@ def test_simple_lab() -> None:
|
|||
# not a linear luminance map. so L != 128:
|
||||
assert k == (137, 128, 128)
|
||||
|
||||
l_data = i_lab.getdata(0)
|
||||
a_data = i_lab.getdata(1)
|
||||
b_data = i_lab.getdata(2)
|
||||
l_data = i_lab.get_flattened_data(0)
|
||||
a_data = i_lab.get_flattened_data(1)
|
||||
b_data = i_lab.get_flattened_data(2)
|
||||
|
||||
assert list(l_data) == [137] * 100
|
||||
assert list(a_data) == [128] * 100
|
||||
assert list(b_data) == [128] * 100
|
||||
assert l_data == (137,) * 100
|
||||
assert a_data == (128,) * 100
|
||||
assert b_data == (128,) * 100
|
||||
|
||||
|
||||
def test_lab_color() -> None:
|
||||
|
|
|
|||
|
|
@ -68,10 +68,22 @@ def test_sanity() -> None:
|
|||
draw.rectangle(list(range(4)))
|
||||
|
||||
|
||||
def test_valueerror() -> None:
|
||||
def test_new_color() -> None:
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
draw = ImageDraw.Draw(im)
|
||||
assert im.palette is not None
|
||||
assert len(im.palette.colors) == 249
|
||||
|
||||
# Test drawing a new color onto the palette
|
||||
draw.line((0, 0), fill=(0, 0, 0))
|
||||
assert im.palette is not None
|
||||
assert len(im.palette.colors) == 250
|
||||
assert im.palette.dirty
|
||||
|
||||
# Test drawing another new color, now that the palette is dirty
|
||||
draw.point((0, 0), fill=(1, 0, 0))
|
||||
assert len(im.palette.colors) == 251
|
||||
assert im.convert("RGB").getpixel((0, 0)) == (1, 0, 0)
|
||||
|
||||
|
||||
def test_mode_mismatch() -> None:
|
||||
|
|
@ -198,10 +210,10 @@ def test_bitmap() -> None:
|
|||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
with Image.open("Tests/images/pil123rgba.png") as small:
|
||||
small = small.resize((50, 50), Image.Resampling.NEAREST)
|
||||
small_resized = small.resize((50, 50), Image.Resampling.NEAREST)
|
||||
|
||||
# Act
|
||||
draw.bitmap((10, 10), small)
|
||||
draw.bitmap((10, 10), small_resized)
|
||||
|
||||
# Assert
|
||||
assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png")
|
||||
|
|
|
|||
|
|
@ -702,7 +702,7 @@ def test_variation_get(font: ImageFont.FreeTypeFont) -> None:
|
|||
font.get_variation_axes()
|
||||
|
||||
font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf")
|
||||
assert font.get_variation_names(), [
|
||||
assert font.get_variation_names() == [
|
||||
b"ExtraLight",
|
||||
b"Light",
|
||||
b"Regular",
|
||||
|
|
@ -742,6 +742,21 @@ def test_variation_get(font: ImageFont.FreeTypeFont) -> None:
|
|||
]
|
||||
|
||||
|
||||
def test_variation_duplicates() -> None:
|
||||
font = ImageFont.truetype("Tests/fonts/AdobeVFPrototypeDuplicates.ttf")
|
||||
assert font.get_variation_names() == [
|
||||
b"ExtraLight",
|
||||
b"Light",
|
||||
b"Regular",
|
||||
b"Semibold",
|
||||
b"Bold",
|
||||
b"Black",
|
||||
b"Black Medium Contrast",
|
||||
b"Black High Contrast",
|
||||
b"Default",
|
||||
]
|
||||
|
||||
|
||||
def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None:
|
||||
im = Image.new("RGB", (100, 75), "white")
|
||||
d = ImageDraw.Draw(im)
|
||||
|
|
|
|||
|
|
@ -15,13 +15,10 @@ def string_to_img(image_string: str) -> Image.Image:
|
|||
rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)]
|
||||
height = len(rows)
|
||||
width = len(rows[0])
|
||||
im = Image.new("L", (width, height))
|
||||
for i in range(width):
|
||||
for j in range(height):
|
||||
c = rows[j][i]
|
||||
v = c in "X1"
|
||||
im.putpixel((i, j), v)
|
||||
|
||||
im = Image.new("1", (width, height))
|
||||
for x in range(width):
|
||||
for y in range(height):
|
||||
im.putpixel((x, y), rows[y][x] in "X1")
|
||||
return im
|
||||
|
||||
|
||||
|
|
@ -42,10 +39,10 @@ def img_to_string(im: Image.Image) -> str:
|
|||
"""Turn a (small) binary image into a string representation"""
|
||||
chars = ".1"
|
||||
result = []
|
||||
for r in range(im.height):
|
||||
for y in range(im.height):
|
||||
line = ""
|
||||
for c in range(im.width):
|
||||
value = im.getpixel((c, r))
|
||||
for x in range(im.width):
|
||||
value = im.getpixel((x, y))
|
||||
assert not isinstance(value, tuple)
|
||||
assert value is not None
|
||||
line += chars[value > 0]
|
||||
|
|
@ -165,10 +162,12 @@ def test_edge() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_corner() -> None:
|
||||
@pytest.mark.parametrize("mode", ("1", "L"))
|
||||
def test_corner(mode: str) -> None:
|
||||
# Create a corner detector pattern
|
||||
mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"])
|
||||
count, Aout = mop.apply(A)
|
||||
image = A.convert(mode) if mode == "L" else A
|
||||
count, Aout = mop.apply(image)
|
||||
assert count == 5
|
||||
assert_img_equal_img_string(
|
||||
Aout,
|
||||
|
|
@ -184,7 +183,7 @@ def test_corner() -> None:
|
|||
)
|
||||
|
||||
# Test the coordinate counting with the same operator
|
||||
coords = mop.match(A)
|
||||
coords = mop.match(image)
|
||||
assert len(coords) == 4
|
||||
assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4))
|
||||
|
||||
|
|
@ -232,15 +231,15 @@ def test_negate() -> None:
|
|||
|
||||
|
||||
def test_incorrect_mode() -> None:
|
||||
im = hopper("RGB")
|
||||
mop = ImageMorph.MorphOp(op_name="erosion8")
|
||||
|
||||
with pytest.raises(ValueError, match="Image mode must be L"):
|
||||
mop.apply(im)
|
||||
with pytest.raises(ValueError, match="Image mode must be L"):
|
||||
mop.match(im)
|
||||
with pytest.raises(ValueError, match="Image mode must be L"):
|
||||
mop.get_on_pixels(im)
|
||||
with hopper() as im:
|
||||
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
|
||||
mop.apply(im)
|
||||
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
|
||||
mop.match(im)
|
||||
with pytest.raises(ValueError, match="Image mode must be 1 or L"):
|
||||
mop.get_on_pixels(im)
|
||||
|
||||
|
||||
def test_add_patterns() -> None:
|
||||
|
|
@ -281,6 +280,11 @@ def test_pattern_syntax_error(pattern: str) -> None:
|
|||
lb.build_lut()
|
||||
|
||||
|
||||
def test_build_default_lut() -> None:
|
||||
lb = ImageMorph.LutBuilder(op_name="corner")
|
||||
assert lb.build_default_lut() == lb.lut
|
||||
|
||||
|
||||
def test_load_invalid_mrl() -> None:
|
||||
# Arrange
|
||||
invalid_mrl = "Tests/images/hopper.png"
|
||||
|
|
|
|||
|
|
@ -261,10 +261,10 @@ def test_colorize_2color() -> None:
|
|||
|
||||
# Open test image (256px by 10px, black to white)
|
||||
with Image.open("Tests/images/bw_gradient.png") as im:
|
||||
im = im.convert("L")
|
||||
im_l = im.convert("L")
|
||||
|
||||
# Create image with original 2-color functionality
|
||||
im_test = ImageOps.colorize(im, "red", "green")
|
||||
im_test = ImageOps.colorize(im_l, "red", "green")
|
||||
|
||||
# Test output image (2-color)
|
||||
left = (0, 1)
|
||||
|
|
@ -301,11 +301,11 @@ def test_colorize_2color_offset() -> None:
|
|||
|
||||
# Open test image (256px by 10px, black to white)
|
||||
with Image.open("Tests/images/bw_gradient.png") as im:
|
||||
im = im.convert("L")
|
||||
im_l = im.convert("L")
|
||||
|
||||
# Create image with original 2-color functionality with offsets
|
||||
im_test = ImageOps.colorize(
|
||||
im, black="red", white="green", blackpoint=50, whitepoint=100
|
||||
im_l, black="red", white="green", blackpoint=50, whitepoint=100
|
||||
)
|
||||
|
||||
# Test output image (2-color) with offsets
|
||||
|
|
@ -343,11 +343,11 @@ def test_colorize_3color_offset() -> None:
|
|||
|
||||
# Open test image (256px by 10px, black to white)
|
||||
with Image.open("Tests/images/bw_gradient.png") as im:
|
||||
im = im.convert("L")
|
||||
im_l = im.convert("L")
|
||||
|
||||
# Create image with new three color functionality with offsets
|
||||
im_test = ImageOps.colorize(
|
||||
im,
|
||||
im_l,
|
||||
black="red",
|
||||
white="green",
|
||||
mid="blue",
|
||||
|
|
@ -457,9 +457,9 @@ def test_exif_transpose() -> None:
|
|||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
# Orientation set directly on Image.Exif
|
||||
im = hopper()
|
||||
im.getexif()[0x0112] = 3
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
im1 = hopper()
|
||||
im1.getexif()[0x0112] = 3
|
||||
transposed_im = ImageOps.exif_transpose(im1)
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@ def test_getcolor() -> None:
|
|||
palette.getcolor("unknown") # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_getcolor_rgba() -> None:
|
||||
palette = ImagePalette.ImagePalette("RGBA", (1, 2, 3, 4))
|
||||
palette.getcolor((5, 6, 7, 8))
|
||||
assert palette.palette == b"\x01\x02\x03\x04\x05\x06\x07\x08"
|
||||
|
||||
|
||||
def test_getcolor_rgba_color_rgb_palette() -> None:
|
||||
palette = ImagePalette.ImagePalette("RGB")
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageText
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageText, features
|
||||
|
||||
from .helper import assert_image_similar_tofile, skip_unless_feature
|
||||
|
||||
|
|
@ -20,37 +20,75 @@ def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout:
|
|||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont:
|
||||
return ImageFont.truetype(FONT_PATH, 20, layout_engine=layout_engine)
|
||||
@pytest.fixture(
|
||||
scope="module",
|
||||
params=[
|
||||
None,
|
||||
pytest.param(ImageFont.Layout.BASIC, marks=skip_unless_feature("freetype2")),
|
||||
pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
|
||||
],
|
||||
)
|
||||
def font(
|
||||
request: pytest.FixtureRequest,
|
||||
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont:
|
||||
layout_engine = request.param
|
||||
if layout_engine is None:
|
||||
return ImageFont.load_default_imagefont()
|
||||
else:
|
||||
return ImageFont.truetype(FONT_PATH, 20, layout_engine=layout_engine)
|
||||
|
||||
|
||||
def test_get_length(font: ImageFont.FreeTypeFont) -> None:
|
||||
assert ImageText.Text("A", font).get_length() == 12
|
||||
assert ImageText.Text("AB", font).get_length() == 24
|
||||
assert ImageText.Text("M", font).get_length() == 12
|
||||
assert ImageText.Text("y", font).get_length() == 12
|
||||
assert ImageText.Text("a", font).get_length() == 12
|
||||
def test_get_length(font: ImageFont.ImageFont | ImageFont.FreeTypeFont) -> None:
|
||||
factor = 1 if isinstance(font, ImageFont.ImageFont) else 2
|
||||
assert ImageText.Text("A", font).get_length() == 6 * factor
|
||||
assert ImageText.Text("AB", font).get_length() == 12 * factor
|
||||
assert ImageText.Text("M", font).get_length() == 6 * factor
|
||||
assert ImageText.Text("y", font).get_length() == 6 * factor
|
||||
assert ImageText.Text("a", font).get_length() == 6 * factor
|
||||
|
||||
text = ImageText.Text("\n", font)
|
||||
with pytest.raises(ValueError, match="can't measure length of multiline text"):
|
||||
text.get_length()
|
||||
|
||||
|
||||
def test_get_bbox(font: ImageFont.FreeTypeFont) -> None:
|
||||
assert ImageText.Text("A", font).get_bbox() == (0, 4, 12, 16)
|
||||
assert ImageText.Text("AB", font).get_bbox() == (0, 4, 24, 16)
|
||||
assert ImageText.Text("M", font).get_bbox() == (0, 4, 12, 16)
|
||||
assert ImageText.Text("y", font).get_bbox() == (0, 7, 12, 20)
|
||||
assert ImageText.Text("a", font).get_bbox() == (0, 7, 12, 16)
|
||||
@pytest.mark.parametrize(
|
||||
"text, expected",
|
||||
(
|
||||
("A", (0, 4, 12, 16)),
|
||||
("AB", (0, 4, 24, 16)),
|
||||
("M", (0, 4, 12, 16)),
|
||||
("y", (0, 7, 12, 20)),
|
||||
("a", (0, 7, 12, 16)),
|
||||
),
|
||||
)
|
||||
def test_get_bbox(
|
||||
font: ImageFont.ImageFont | ImageFont.FreeTypeFont,
|
||||
text: str,
|
||||
expected: tuple[int, int, int, int],
|
||||
) -> None:
|
||||
if isinstance(font, ImageFont.ImageFont):
|
||||
expected = (0, 0, expected[2] // 2, 11)
|
||||
assert ImageText.Text(text, font).get_bbox() == expected
|
||||
|
||||
|
||||
def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None:
|
||||
font = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
|
||||
text = ImageText.Text("Hello World!", font)
|
||||
text.embed_color()
|
||||
if features.check_module("freetype2"):
|
||||
font = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
|
||||
text = ImageText.Text("Hello World!", font)
|
||||
text.embed_color()
|
||||
assert text.get_length() == 288
|
||||
|
||||
im = Image.new("RGB", (300, 64), "white")
|
||||
draw = ImageDraw.Draw(im)
|
||||
draw.text((10, 10), text, "#fa6")
|
||||
im = Image.new("RGB", (300, 64), "white")
|
||||
draw = ImageDraw.Draw(im)
|
||||
draw.text((10, 10), text, "#fa6")
|
||||
|
||||
assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1)
|
||||
assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1)
|
||||
|
||||
text = ImageText.Text("", mode="1")
|
||||
with pytest.raises(
|
||||
ValueError, match="Embedded color supported only in RGB and RGBA modes"
|
||||
):
|
||||
text.embed_color()
|
||||
|
||||
|
||||
@skip_unless_feature("freetype2")
|
||||
|
|
|
|||
|
|
@ -20,21 +20,19 @@ TEST_IMAGE_SIZE = (10, 10)
|
|||
|
||||
def test_numpy_to_image() -> None:
|
||||
def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image:
|
||||
data = tuple(range(100))
|
||||
if bands == 1:
|
||||
if boolean:
|
||||
data = [0, 255] * 50
|
||||
else:
|
||||
data = list(range(100))
|
||||
data = (0, 255) * 50
|
||||
a = numpy.array(data, dtype=dtype)
|
||||
a.shape = TEST_IMAGE_SIZE
|
||||
i = Image.fromarray(a)
|
||||
assert list(i.getdata()) == data
|
||||
assert i.get_flattened_data() == data
|
||||
else:
|
||||
data = list(range(100))
|
||||
a = numpy.array([[x] * bands for x in data], dtype=dtype)
|
||||
a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands
|
||||
i = Image.fromarray(a)
|
||||
assert list(i.getchannel(0).getdata()) == list(range(100))
|
||||
assert i.get_flattened_data(0) == tuple(range(100))
|
||||
return i
|
||||
|
||||
# Check supported 1-bit integer formats
|
||||
|
|
@ -191,7 +189,7 @@ def test_putdata() -> None:
|
|||
arr = numpy.zeros((15000,), numpy.float32)
|
||||
im.putdata(arr)
|
||||
|
||||
assert len(im.getdata()) == len(arr)
|
||||
assert len(im.get_flattened_data()) == len(arr)
|
||||
|
||||
|
||||
def test_resize() -> None:
|
||||
|
|
@ -248,7 +246,7 @@ def test_bool() -> None:
|
|||
a[0][0] = True
|
||||
|
||||
im2 = Image.fromarray(a)
|
||||
assert im2.getdata()[0] == 255
|
||||
assert im2.getpixel((0, 0)) == 255
|
||||
|
||||
|
||||
def test_no_resource_warning_for_numpy_array() -> None:
|
||||
|
|
|
|||
|
|
@ -19,30 +19,28 @@ def helper_pickle_file(
|
|||
# Arrange
|
||||
with Image.open(test_file) as im:
|
||||
filename = tmp_path / "temp.pkl"
|
||||
if mode:
|
||||
im = im.convert(mode)
|
||||
converted_im = im.convert(mode) if mode else im
|
||||
|
||||
# Act
|
||||
with open(filename, "wb") as f:
|
||||
pickle.dump(im, f, protocol)
|
||||
pickle.dump(converted_im, f, protocol)
|
||||
with open(filename, "rb") as f:
|
||||
loaded_im = pickle.load(f)
|
||||
|
||||
# Assert
|
||||
assert im == loaded_im
|
||||
assert converted_im == loaded_im
|
||||
|
||||
|
||||
def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
if mode:
|
||||
im = im.convert(mode)
|
||||
converted_im = im.convert(mode) if mode else im
|
||||
|
||||
# Act
|
||||
dumped_string = pickle.dumps(im, protocol)
|
||||
dumped_string = pickle.dumps(converted_im, protocol)
|
||||
loaded_im = pickle.loads(dumped_string)
|
||||
|
||||
# Assert
|
||||
assert im == loaded_im
|
||||
assert converted_im == loaded_im
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -90,18 +88,18 @@ def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
|
|||
# Arrange
|
||||
filename = tmp_path / "temp.pkl"
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
im = im.convert("PA")
|
||||
im_pa = im.convert("PA")
|
||||
|
||||
# Act / Assert
|
||||
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
|
||||
im._mode = "LA"
|
||||
im_pa._mode = "LA"
|
||||
with open(filename, "wb") as f:
|
||||
pickle.dump(im, f, protocol)
|
||||
pickle.dump(im_pa, f, protocol)
|
||||
with open(filename, "rb") as f:
|
||||
loaded_im = pickle.load(f)
|
||||
|
||||
im._mode = "PA"
|
||||
assert im == loaded_im
|
||||
im_pa._mode = "PA"
|
||||
assert im_pa == loaded_im
|
||||
|
||||
|
||||
@skip_unless_feature("webp")
|
||||
|
|
|
|||
|
|
@ -6,10 +6,15 @@ import pytest
|
|||
|
||||
from PIL import __version__
|
||||
|
||||
TYPE_CHECKING = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from importlib.metadata import PackageMetadata
|
||||
|
||||
pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed")
|
||||
|
||||
|
||||
def map_metadata_keys(md):
|
||||
def map_metadata_keys(md: PackageMetadata) -> dict[str, str | list[str] | None]:
|
||||
# Convert installed wheel metadata into canonical Core Metadata 2.4 format.
|
||||
# This was a utility method in pyroma 4.3.3; it was removed in 5.0.
|
||||
# This implementation is constructed from the relevant logic from
|
||||
|
|
@ -17,16 +22,16 @@ def map_metadata_keys(md):
|
|||
# upstream to Pyroma as https://github.com/regebro/pyroma/pull/116,
|
||||
# so it may be possible to simplify this test in future.
|
||||
data = {}
|
||||
for key in set(md.keys()):
|
||||
for key in set(md):
|
||||
value = md.get_all(key)
|
||||
key = pyroma.projectdata.normalize(key)
|
||||
|
||||
if len(value) == 1:
|
||||
value = value[0]
|
||||
if value.strip() == "UNKNOWN":
|
||||
continue
|
||||
|
||||
data[key] = value
|
||||
if value is not None and len(value) == 1:
|
||||
first_value = value[0]
|
||||
if first_value.strip() != "UNKNOWN":
|
||||
data[key] = first_value
|
||||
else:
|
||||
data[key] = value
|
||||
return data
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -49,11 +49,13 @@ class TestShellInjection:
|
|||
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
|
||||
def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None:
|
||||
with Image.open(TEST_GIF) as im:
|
||||
im = im.convert("RGB")
|
||||
self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm)
|
||||
im_rgb = im.convert("RGB")
|
||||
self.assert_save_filename_check(
|
||||
tmp_path, im_rgb, GifImagePlugin._save_netpbm
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
|
||||
def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None:
|
||||
with Image.open(TEST_GIF) as im:
|
||||
im = im.convert("L")
|
||||
self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm)
|
||||
im_l = im.convert("L")
|
||||
self.assert_save_filename_check(tmp_path, im_l, GifImagePlugin._save_netpbm)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# install libimagequant
|
||||
|
||||
archive_name=libimagequant
|
||||
archive_version=4.4.0
|
||||
archive_version=4.4.1
|
||||
|
||||
archive=$archive_name-$archive_version
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import subprocess
|
|||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from sphinx.application import Sphinx
|
||||
from typing import Any
|
||||
|
||||
DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+")
|
||||
VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n")
|
||||
|
|
@ -28,7 +28,7 @@ def get_date_for(git_version: str) -> str | None:
|
|||
return out.split()[0]
|
||||
|
||||
|
||||
def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None:
|
||||
def add_date(app: Any, doc_name: str, source: list[str]) -> None:
|
||||
if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])):
|
||||
old_title = m.group(1)
|
||||
|
||||
|
|
@ -43,6 +43,6 @@ def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None:
|
|||
source[0] = result
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> dict[str, bool]:
|
||||
def setup(app: Any) -> dict[str, bool]:
|
||||
app.connect("source-read", add_date)
|
||||
return {"parallel_read_safe": True}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,16 @@ Image._show
|
|||
``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15).
|
||||
Use :py:meth:`~PIL.ImageShow.show` instead.
|
||||
|
||||
Image getdata()
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 12.1.0
|
||||
|
||||
:py:meth:`~PIL.Image.Image.getdata` has been deprecated.
|
||||
:py:meth:`~PIL.Image.Image.get_flattened_data` can be used instead. This new method is
|
||||
identical, except that it returns a tuple of pixel values, instead of an internal
|
||||
Pillow data type.
|
||||
|
||||
Removed features
|
||||
----------------
|
||||
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
format_description = "DirectDraw Surface"
|
||||
|
||||
def _open(self) -> None:
|
||||
assert self.fp is not None
|
||||
if not _accept(self.fp.read(4)):
|
||||
msg = "not a DDS file"
|
||||
raise SyntaxError(msg)
|
||||
|
|
|
|||
|
|
@ -999,7 +999,7 @@ where applicable:
|
|||
The number of times to loop this APNG, 0 indicates infinite looping.
|
||||
|
||||
**duration**
|
||||
The time to display this APNG frame (in milliseconds).
|
||||
The time to display this APNG frame (in milliseconds), given as a float.
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
@ -1041,9 +1041,8 @@ following parameters can also be set:
|
|||
Defaults to 0.
|
||||
|
||||
**duration**
|
||||
Integer (or list or tuple of integers) length of time to display this APNG frame
|
||||
(in milliseconds).
|
||||
Defaults to 0.
|
||||
The length of time (or list or tuple of lengths of time) to display this APNG frame
|
||||
(in milliseconds). Defaults to 0.
|
||||
|
||||
**disposal**
|
||||
An integer (or list or tuple of integers) specifying the APNG disposal
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
* **libimagequant** provides improved color quantization
|
||||
|
||||
* Pillow has been tested with libimagequant **2.6-4.4.0**
|
||||
* Pillow has been tested with libimagequant **2.6-4.4.1**
|
||||
* Libimagequant is licensed GPLv3, which is more restrictive than
|
||||
the Pillow license, therefore we will not be distributing binaries
|
||||
with libimagequant support enabled.
|
||||
|
|
@ -116,7 +116,7 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions.
|
||||
|
||||
Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with::
|
||||
Prerequisites for **Ubuntu 16.04 LTS - 24.04 LTS** are installed with::
|
||||
|
||||
sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
|
||||
libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ These platforms are built and tested for every change.
|
|||
+----------------------------------+----------------------------+---------------------+
|
||||
| Debian 13 Trixie | 3.13 | x86, x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 41 | 3.13 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 42 | 3.13 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 43 | 3.14 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Gentoo | 3.12 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| macOS 15 Sequoia | 3.10 | x86-64 |
|
||||
|
|
@ -53,8 +53,8 @@ These platforms are built and tested for every change.
|
|||
| | | s390x |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Windows Server 2022 | 3.10 | x86 |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.11, 3.12, 3.13, 3.14, | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 |
|
||||
| | PyPy3 | |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.12 (MinGW) | x86-64 |
|
||||
|
|
@ -71,100 +71,102 @@ These platforms have been reported to work at the versions mentioned.
|
|||
Contributors please test Pillow on your platform then update this
|
||||
document and send a pull request.
|
||||
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||
| | | versions | | Pillow version | | processors |
|
||||
+==================================+============================+==================+==============+
|
||||
| macOS 26 Tahoe | 3.9, 3.10, 3.11, 3.12, 3.13| 11.3.0 |arm |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.3.0 |arm |
|
||||
| +----------------------------+------------------+ |
|
||||
| | 3.8 | 10.4.0 | |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.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 |
|
||||
| +----------------------------+------------------+--------------+
|
||||
| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 |
|
||||
| +----------------------------+------------------+ |
|
||||
| | 3.6 | 8.4.0 | |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 |
|
||||
| +----------------------------+------------------+ |
|
||||
| | 3.5 | 7.2.0 | |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 |
|
||||
| +----------------------------+------------------+ |
|
||||
| | 2.7 | 6.0.0 | |
|
||||
| +----------------------------+------------------+ |
|
||||
| | 3.4 | 5.4.1 | |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 |
|
||||
| +----------------------------+------------------+ |
|
||||
| | 3.3 | 4.1.0 | |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Redhat Linux 6 | 2.6 | |x86 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| CentOS 6.3 | 2.7, 3.3 | |x86 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| CentOS 8 | 3.9 | 9.0.0 |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 |
|
||||
| | | PyPy5.3.1, PyPy3 v2.4.0 | | |
|
||||
| +----------------------------+------------------+--------------+
|
||||
| | 2.7 | 4.3.0 |x86-64 |
|
||||
| +----------------------------+------------------+--------------+
|
||||
| | 2.7, 3.2 | 3.4.1 |ppc |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm |
|
||||
| +----------------------------+------------------+ |
|
||||
| | 2.7 | 6.2.2 | |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Windows 11 23H2 | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Windows 10 | 3.7 | 7.1.0 |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||
| | | versions | | Pillow version | | processors |
|
||||
+==================================+=============================+==================+==============+
|
||||
| macOS 26 Tahoe | 3.10, 3.11, 3.12, 3.13, 3.14| 12.0.0 |arm |
|
||||
| +-----------------------------+------------------+ |
|
||||
| | 3.9 | 11.3.0 | |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13 | 11.3.0 |arm |
|
||||
| +-----------------------------+------------------+ |
|
||||
| | 3.8 | 10.4.0 | |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.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 |
|
||||
| +-----------------------------+------------------+--------------+
|
||||
| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 |
|
||||
| +-----------------------------+------------------+ |
|
||||
| | 3.6 | 8.4.0 | |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 |
|
||||
| +-----------------------------+------------------+ |
|
||||
| | 3.5 | 7.2.0 | |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 |
|
||||
| +-----------------------------+------------------+ |
|
||||
| | 2.7 | 6.0.0 | |
|
||||
| +-----------------------------+------------------+ |
|
||||
| | 3.4 | 5.4.1 | |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 |
|
||||
| +-----------------------------+------------------+ |
|
||||
| | 3.3 | 4.1.0 | |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Redhat Linux 6 | 2.6 | |x86 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| CentOS 6.3 | 2.7, 3.3 | |x86 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| CentOS 8 | 3.9 | 9.0.0 |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 |
|
||||
| | | PyPy5.3.1, PyPy3 v2.4.0 | | |
|
||||
| +-----------------------------+------------------+--------------+
|
||||
| | 2.7 | 4.3.0 |x86-64 |
|
||||
| +-----------------------------+------------------+--------------+
|
||||
| | 2.7, 3.2 | 3.4.1 |ppc |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm |
|
||||
| +-----------------------------+------------------+ |
|
||||
| | 2.7 | 6.2.2 | |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Windows 11 23H2 | 3.9, 3.10, 3.11, 3.12, 3.13 | 11.0.0 |arm64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Windows 11 Pro | 3.11, 3.12 | 10.2.0 |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Windows 10 | 3.7 | 7.1.0 |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 |
|
||||
+----------------------------------+-----------------------------+------------------+--------------+
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ This helps to get the bounding box coordinates of the input image::
|
|||
.. automethod:: PIL.Image.Image.getchannel
|
||||
.. automethod:: PIL.Image.Image.getcolors
|
||||
.. automethod:: PIL.Image.Image.getdata
|
||||
.. automethod:: PIL.Image.Image.get_flattened_data
|
||||
.. automethod:: PIL.Image.Image.getexif
|
||||
.. automethod:: PIL.Image.Image.getextrema
|
||||
.. automethod:: PIL.Image.Image.getpalette
|
||||
|
|
|
|||
|
|
@ -44,9 +44,11 @@ or the clipboard to a PIL image memory.
|
|||
.. versionadded:: 7.1.0
|
||||
|
||||
:param window:
|
||||
HWND, to capture a single window. Windows only.
|
||||
Capture a single window. On Windows, this is a HWND. On macOS, this is a
|
||||
CGWindowID.
|
||||
|
||||
.. versionadded:: 11.2.1
|
||||
.. versionadded:: 11.2.1 Windows support
|
||||
.. versionadded:: 12.1.0 macOS support
|
||||
:return: An image
|
||||
|
||||
.. py:function:: grabclipboard()
|
||||
|
|
|
|||