Merge branch 'main' into add-cygwin-to-ci
|
@ -25,8 +25,8 @@ install:
|
||||||
- mv c:\pillow-depends-main c:\pillow-depends
|
- mv c:\pillow-depends-main c:\pillow-depends
|
||||||
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
|
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
|
||||||
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
|
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
|
||||||
- ..\pillow-depends\gs9550w32.exe /S
|
- ..\pillow-depends\gs9561w32.exe /S
|
||||||
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.55.0\bin;%PATH%
|
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH%
|
||||||
- cd c:\pillow\winbuild\
|
- cd c:\pillow\winbuild\
|
||||||
- ps: |
|
- ps: |
|
||||||
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
||||||
|
@ -43,7 +43,7 @@ build_script:
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
- cd c:\pillow
|
- cd c:\pillow
|
||||||
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov'
|
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout'
|
||||||
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
|
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
|
||||||
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
|
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
|
||||||
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
|
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
|
||||||
|
|
4
.github/workflows/cifuzz.yml
vendored
|
@ -31,13 +31,13 @@ jobs:
|
||||||
language: python
|
language: python
|
||||||
dry-run: false
|
dry-run: false
|
||||||
- name: Upload New Crash
|
- name: Upload New Crash
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
if: failure() && steps.build.outcome == 'success'
|
if: failure() && steps.build.outcome == 'success'
|
||||||
with:
|
with:
|
||||||
name: artifacts
|
name: artifacts
|
||||||
path: ./out/artifacts
|
path: ./out/artifacts
|
||||||
- name: Upload Legacy Crash
|
- name: Upload Legacy Crash
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
if: steps.run.outcome == 'success'
|
if: steps.run.outcome == 'success'
|
||||||
with:
|
with:
|
||||||
name: crash
|
name: crash
|
||||||
|
|
4
.github/workflows/lint.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
||||||
name: Lint
|
name: Lint
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: pre-commit cache
|
- name: pre-commit cache
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
|
@ -21,7 +21,7 @@ jobs:
|
||||||
lint-pre-commit-
|
lint-pre-commit-
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
cache: pip
|
cache: pip
|
||||||
|
|
27
.github/workflows/stale.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
name: Close stale issues
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "10 0 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
if: github.repository_owner == 'python-pillow'
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: "Check issues"
|
||||||
|
uses: actions/stale@v5
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
only-labels: "Awaiting OP Action"
|
||||||
|
close-issue-message: "Closing this issue as no feedback has been received."
|
||||||
|
days-before-stale: 7
|
||||||
|
days-before-issue-close: 0
|
||||||
|
days-before-pr-close: -1
|
||||||
|
labels-to-remove-when-unstale: "Awaiting OP Action"
|
3
.github/workflows/test-docker.yml
vendored
|
@ -23,7 +23,6 @@ jobs:
|
||||||
centos-stream-9-amd64,
|
centos-stream-9-amd64,
|
||||||
debian-10-buster-x86,
|
debian-10-buster-x86,
|
||||||
debian-11-bullseye-x86,
|
debian-11-bullseye-x86,
|
||||||
fedora-34-amd64,
|
|
||||||
fedora-35-amd64,
|
fedora-35-amd64,
|
||||||
gentoo,
|
gentoo,
|
||||||
ubuntu-18.04-bionic-amd64,
|
ubuntu-18.04-bionic-amd64,
|
||||||
|
@ -41,7 +40,7 @@ jobs:
|
||||||
name: ${{ matrix.docker }}
|
name: ${{ matrix.docker }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build system information
|
- name: Build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
run: python3 .github/workflows/system-info.py
|
||||||
|
|
2
.github/workflows/test-mingw.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Pillow
|
- name: Checkout Pillow
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up shell
|
- name: Set up shell
|
||||||
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
|
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
|
||||||
|
|
2
.github/workflows/test-valgrind.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
||||||
name: ${{ matrix.docker }}
|
name: ${{ matrix.docker }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build system information
|
- name: Build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
run: python3 .github/workflows/system-info.py
|
||||||
|
|
14
.github/workflows/test-windows.yml
vendored
|
@ -23,17 +23,17 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Pillow
|
- name: Checkout Pillow
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Checkout cached dependencies
|
- name: Checkout cached dependencies
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
repository: python-pillow/pillow-depends
|
repository: python-pillow/pillow-depends
|
||||||
path: winbuild\depends
|
path: winbuild\depends
|
||||||
|
|
||||||
# sets env: pythonLocation
|
# sets env: pythonLocation
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
architecture: ${{ matrix.architecture }}
|
architecture: ${{ matrix.architecture }}
|
||||||
|
@ -52,8 +52,8 @@ jobs:
|
||||||
7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
|
7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
|
||||||
echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
|
echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
winbuild\depends\gs9550w32.exe /S
|
winbuild\depends\gs9561w32.exe /S
|
||||||
echo "C:\Program Files (x86)\gs\gs9.55.0\bin" >> $env:GITHUB_PATH
|
echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
xcopy /S /Y winbuild\depends\test_images\* Tests\images\
|
xcopy /S /Y winbuild\depends\test_images\* Tests\images\
|
||||||
|
|
||||||
|
@ -156,7 +156,7 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Upload errors
|
- name: Upload errors
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: errors
|
name: errors
|
||||||
|
@ -182,7 +182,7 @@ jobs:
|
||||||
winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
|
winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
|
||||||
shell: cmd
|
shell: cmd
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v3
|
||||||
if: "github.event_name != 'pull_request'"
|
if: "github.event_name != 'pull_request'"
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.wheel.outputs.dist }}
|
name: ${{ steps.wheel.outputs.dist }}
|
||||||
|
|
8
.github/workflows/test.yml
vendored
|
@ -36,10 +36,10 @@ jobs:
|
||||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
cache: pip
|
cache: pip
|
||||||
|
@ -84,7 +84,7 @@ jobs:
|
||||||
mkdir -p Tests/errors
|
mkdir -p Tests/errors
|
||||||
|
|
||||||
- name: Upload errors
|
- name: Upload errors
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: errors
|
name: errors
|
||||||
|
@ -93,7 +93,7 @@ jobs:
|
||||||
- name: Docs
|
- name: Docs
|
||||||
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
|
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph
|
python3 -m pip install furo sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph
|
||||||
make doccheck
|
make doccheck
|
||||||
|
|
||||||
- name: After success
|
- name: After success
|
||||||
|
|
2
.github/workflows/tidelift.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Scan
|
- name: Scan
|
||||||
uses: tidelift/alignment-action@main
|
uses: tidelift/alignment-action@main
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: f1d4e742c91dd5179d742b0db9293c4472b765f8 # frozen: 21.12b0
|
rev: 22.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: ["--target-version", "py37"]
|
args: ["--target-version", "py37"]
|
||||||
|
@ -9,35 +9,35 @@ repos:
|
||||||
types: []
|
types: []
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/isort
|
- repo: https://github.com/PyCQA/isort
|
||||||
rev: c5e8fa75dda5f764d20f66a215d71c21cfa198e1 # frozen: 5.10.1
|
rev: 5.10.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
|
|
||||||
- repo: https://github.com/asottile/yesqa
|
- repo: https://github.com/asottile/yesqa
|
||||||
rev: 35cf7dc24fa922927caded7a21b2a8cb04bf8e10 # frozen: v1.3.0
|
rev: v1.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: yesqa
|
- id: yesqa
|
||||||
|
|
||||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||||
rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10
|
rev: v1.1.13
|
||||||
hooks:
|
hooks:
|
||||||
- id: remove-tabs
|
- id: remove-tabs
|
||||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: cbeb4c9c4137cff1568659fcc48e8b85cddd0c8d # frozen: 4.0.1
|
rev: 4.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0
|
rev: v1.9.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: python-check-blanket-noqa
|
- id: python-check-blanket-noqa
|
||||||
- id: rst-backticks
|
- id: rst-backticks
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: 8fe62d14e0b4d7d845a7022c5c2c3ae41bdd3f26 # frozen: v4.1.0
|
rev: v4.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
|
83
CHANGES.rst
|
@ -2,9 +2,90 @@
|
||||||
Changelog (Pillow)
|
Changelog (Pillow)
|
||||||
==================
|
==================
|
||||||
|
|
||||||
9.1.0 (unreleased)
|
9.2.0 (unreleased)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
- Round lut values where necessary #6188
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Load before getting size in resize() #6190
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Load image before performing size calculations in thumbnail() #6186
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Deprecated PhotoImage.paste() box parameter #6178
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
9.1.0 (2022-04-01)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Add support for multiple component transformation to JPEG2000 #5500
|
||||||
|
[scaramallion, radarhere, hugovk]
|
||||||
|
|
||||||
|
- Fix loading FriBiDi on Alpine #6165
|
||||||
|
[nulano]
|
||||||
|
|
||||||
|
- Added setting for converting GIF P frames to RGB #6150
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Allow 1 mode images to be inverted #6034
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Raise ValueError when trying to save empty JPEG #6159
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Always save TIFF with contiguous planar configuration #5973
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Connected discontiguous polygon corners #5980
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Ensure Tkinter hook is activated for getimage() #6032
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Use screencapture arguments to crop on macOS #6152
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Do not mark L mode JPEG as 1 bit in PDF #6151
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added support for reading I;16R TIFF images #6132
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- If an error occurs after creating a file, remove the file #6134
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed calling DisplayViewer or XVViewer without a title #6136
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Retain RGBA transparency when saving multiple GIF frames #6128
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Save additional ICO frames with other bit depths if supplied #6122
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Handle EXIF data truncated to just the header #6124
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added support for reading BMP images with RLE8 compression #6102
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support Python distributions where _tkinter is compiled in #6006
|
||||||
|
[lukegb]
|
||||||
|
|
||||||
|
- Added support for PPM arbitrary maxval #6119
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added BigTIFF reading #6097
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- When converting, clip I;16 to be unsigned, not signed #6112
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed loading L mode GIF with transparency #6086
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
- Improved handling of PPM header #5121
|
- Improved handling of PPM header #5121
|
||||||
[Piolie, radarhere]
|
[Piolie, radarhere]
|
||||||
|
|
||||||
|
|
2
Makefile
|
@ -77,7 +77,7 @@ release-test:
|
||||||
-rm dist/*.egg
|
-rm dist/*.egg
|
||||||
-rmdir dist
|
-rmdir dist
|
||||||
python3 -m pytest -qq
|
python3 -m pytest -qq
|
||||||
python3 -m check-manifest
|
python3 -m check_manifest
|
||||||
python3 -m pyroma .
|
python3 -m pyroma .
|
||||||
$(MAKE) readme
|
$(MAKE) readme
|
||||||
|
|
||||||
|
|
14
RELEASING.md
|
@ -24,13 +24,13 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
||||||
* [ ] Create and check source distribution:
|
* [ ] Create and check source distribution:
|
||||||
```bash
|
```bash
|
||||||
make sdist
|
make sdist
|
||||||
twine check dist/*
|
python3 -m twine check --strict dist/*
|
||||||
```
|
```
|
||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||||
```bash
|
```bash
|
||||||
twine check dist/*
|
python3 -m twine check --strict dist/*
|
||||||
twine upload dist/Pillow-5.2.0*
|
python3 -m twine upload dist/Pillow-5.2.0*
|
||||||
```
|
```
|
||||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||||
* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py`
|
* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py`
|
||||||
|
@ -61,13 +61,13 @@ Released as needed for security, installation or critical bug fixes.
|
||||||
* [ ] Create and check source distribution:
|
* [ ] Create and check source distribution:
|
||||||
```bash
|
```bash
|
||||||
make sdist
|
make sdist
|
||||||
twine check dist/*
|
python3 -m twine check --strict dist/*
|
||||||
```
|
```
|
||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||||
```bash
|
```bash
|
||||||
twine check dist/*
|
python3 -m twine check --strict dist/*
|
||||||
twine upload dist/Pillow-5.2.1*
|
python3 -m twine upload dist/Pillow-5.2.1*
|
||||||
```
|
```
|
||||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ Released as needed privately to individual vendors for critical security-related
|
||||||
* [ ] Create and check source distribution:
|
* [ ] Create and check source distribution:
|
||||||
```bash
|
```bash
|
||||||
make sdist
|
make sdist
|
||||||
twine check dist/*
|
python3 -m twine check --strict dist/*
|
||||||
```
|
```
|
||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||||
|
|
|
@ -4,5 +4,5 @@ import sys
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
if sys.maxsize < 2 ** 32:
|
if sys.maxsize < 2**32:
|
||||||
im = Image.new("L", (999999, 999999), 0)
|
im = Image.new("L", (999999, 999999), 0)
|
||||||
|
|
|
@ -23,7 +23,7 @@ YDIM = 32769
|
||||||
XDIM = 48000
|
XDIM = 48000
|
||||||
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system")
|
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
|
||||||
|
|
||||||
|
|
||||||
def _write_png(tmp_path, xdim, ydim):
|
def _write_png(tmp_path, xdim, ydim):
|
||||||
|
|
|
@ -19,7 +19,7 @@ YDIM = 32769
|
||||||
XDIM = 48000
|
XDIM = 48000
|
||||||
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system")
|
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
|
||||||
|
|
||||||
|
|
||||||
def _write_png(tmp_path, xdim, ydim):
|
def _write_png(tmp_path, xdim, ydim):
|
||||||
|
|
BIN
Tests/images/16bit.r.tif
Normal file
BIN
Tests/images/hopper_bigtiff.tif
Normal file
BIN
Tests/images/hopper_rle8.bmp
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
Tests/images/hopper_rle8_row_overflow.bmp
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
Tests/images/imagedraw/discontiguous_corners_polygon.png
Normal file
After Width: | Height: | Size: 486 B |
BIN
Tests/images/no_palette.gif
Normal file
After Width: | Height: | Size: 48 B |
BIN
Tests/images/no_palette_with_background.gif
Normal file
After Width: | Height: | Size: 54 B |
BIN
Tests/images/no_palette_with_transparency.gif
Normal file
After Width: | Height: | Size: 1.6 KiB |
|
@ -40,6 +40,7 @@ def test_questionable():
|
||||||
"rgb32fakealpha.bmp",
|
"rgb32fakealpha.bmp",
|
||||||
"rgb24largepal.bmp",
|
"rgb24largepal.bmp",
|
||||||
"pal8os2sp.bmp",
|
"pal8os2sp.bmp",
|
||||||
|
"pal8rletrns.bmp",
|
||||||
"rgb32bf-xbgr.bmp",
|
"rgb32bf-xbgr.bmp",
|
||||||
]
|
]
|
||||||
for f in get_files("q"):
|
for f in get_files("q"):
|
||||||
|
|
|
@ -110,9 +110,9 @@ class TestCoreMemory:
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
Image.core.set_blocks_max(-1)
|
Image.core.set_blocks_max(-1)
|
||||||
if sys.maxsize < 2 ** 32:
|
if sys.maxsize < 2**32:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
Image.core.set_blocks_max(2 ** 29)
|
Image.core.set_blocks_max(2**29)
|
||||||
|
|
||||||
@pytest.mark.skipif(is_pypy(), reason="Images not collected")
|
@pytest.mark.skipif(is_pypy(), reason="Images not collected")
|
||||||
def test_set_blocks_max_stats(self):
|
def test_set_blocks_max_stats(self):
|
||||||
|
|
|
@ -4,7 +4,12 @@ import pytest
|
||||||
|
|
||||||
from PIL import BmpImagePlugin, Image
|
from PIL import BmpImagePlugin, Image
|
||||||
|
|
||||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
from .helper import (
|
||||||
|
assert_image_equal,
|
||||||
|
assert_image_equal_tofile,
|
||||||
|
assert_image_similar_tofile,
|
||||||
|
hopper,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_sanity(tmp_path):
|
def test_sanity(tmp_path):
|
||||||
|
@ -125,6 +130,42 @@ def test_rgba_bitfields():
|
||||||
assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
|
assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
|
||||||
|
|
||||||
|
|
||||||
|
def test_rle8():
|
||||||
|
with Image.open("Tests/images/hopper_rle8.bmp") as im:
|
||||||
|
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
|
||||||
|
|
||||||
|
# This test image has been manually hexedited
|
||||||
|
# to have rows with too much data
|
||||||
|
with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im:
|
||||||
|
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
|
||||||
|
|
||||||
|
# Signal end of bitmap before the image is finished
|
||||||
|
with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp:
|
||||||
|
data = fp.read(1063) + b"\x01"
|
||||||
|
with Image.open(io.BytesIO(data)) as im:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"file_name,length",
|
||||||
|
(
|
||||||
|
# EOF immediately after the header
|
||||||
|
("Tests/images/hopper_rle8.bmp", 1078),
|
||||||
|
# EOF during delta
|
||||||
|
("Tests/images/bmp/q/pal8rletrns.bmp", 3670),
|
||||||
|
# EOF when reading data in absolute mode
|
||||||
|
("Tests/images/bmp/g/pal8rle.bmp", 1064),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_rle8_eof(file_name, length):
|
||||||
|
with open(file_name, "rb") as fp:
|
||||||
|
data = fp.read(length)
|
||||||
|
with Image.open(io.BytesIO(data)) as im:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_offset():
|
def test_offset():
|
||||||
# This image has been hexedited
|
# This image has been hexedited
|
||||||
# to exclude the palette size from the pixel data offset
|
# to exclude the palette size from the pixel data offset
|
||||||
|
|
|
@ -196,6 +196,13 @@ def test__accept_false():
|
||||||
assert not output
|
assert not output
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_file():
|
||||||
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
|
with pytest.raises(SyntaxError):
|
||||||
|
DdsImagePlugin.DdsImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
def test_short_header():
|
def test_short_header():
|
||||||
"""Check a short header"""
|
"""Check a short header"""
|
||||||
with open(TEST_FILE_DXT5, "rb") as f:
|
with open(TEST_FILE_DXT5, "rb") as f:
|
||||||
|
|
|
@ -16,6 +16,13 @@ def test_load_dxt1():
|
||||||
assert_image_similar(im, target.convert("RGBA"), 15)
|
assert_image_similar(im, target.convert("RGBA"), 15)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_file():
|
||||||
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
|
with pytest.raises(SyntaxError):
|
||||||
|
FtexImagePlugin.FtexImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
def test_constants_deprecation():
|
def test_constants_deprecation():
|
||||||
for enum, prefix in {
|
for enum, prefix in {
|
||||||
FtexImagePlugin.Format: "FORMAT_",
|
FtexImagePlugin.Format: "FORMAT_",
|
||||||
|
|
|
@ -59,6 +59,51 @@ def test_invalid_file():
|
||||||
GifImagePlugin.GifImageFile(invalid_file)
|
GifImagePlugin.GifImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_l_mode_transparency():
|
||||||
|
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
|
||||||
|
assert im.mode == "L"
|
||||||
|
assert im.load()[0, 0] == 128
|
||||||
|
assert im.info["transparency"] == 255
|
||||||
|
|
||||||
|
im.seek(1)
|
||||||
|
assert im.mode == "L"
|
||||||
|
assert im.load()[0, 0] == 128
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy():
|
||||||
|
with Image.open("Tests/images/chi.gif") as im:
|
||||||
|
expected_zero = im.convert("RGB")
|
||||||
|
|
||||||
|
im.seek(1)
|
||||||
|
expected_one = im.convert("RGB")
|
||||||
|
|
||||||
|
try:
|
||||||
|
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
|
||||||
|
with Image.open("Tests/images/chi.gif") as im:
|
||||||
|
assert im.mode == "RGB"
|
||||||
|
assert_image_equal(im, expected_zero)
|
||||||
|
|
||||||
|
GifImagePlugin.LOADING_STRATEGY = (
|
||||||
|
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
|
||||||
|
)
|
||||||
|
# Stay in P mode with only a global palette
|
||||||
|
with Image.open("Tests/images/chi.gif") as im:
|
||||||
|
assert im.mode == "P"
|
||||||
|
|
||||||
|
im.seek(1)
|
||||||
|
assert im.mode == "P"
|
||||||
|
assert_image_equal(im.convert("RGB"), expected_one)
|
||||||
|
|
||||||
|
# Change to RGB mode when a frame has an individual palette
|
||||||
|
with Image.open("Tests/images/iss634.gif") as im:
|
||||||
|
assert im.mode == "P"
|
||||||
|
|
||||||
|
im.seek(1)
|
||||||
|
assert im.mode == "RGB"
|
||||||
|
finally:
|
||||||
|
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||||
|
|
||||||
|
|
||||||
def test_optimize():
|
def test_optimize():
|
||||||
def test_grayscale(optimize):
|
def test_grayscale(optimize):
|
||||||
im = Image.new("L", (1, 1), 0)
|
im = Image.new("L", (1, 1), 0)
|
||||||
|
@ -383,18 +428,38 @@ def test_dispose_background_transparency():
|
||||||
assert px[35, 30][3] == 0
|
assert px[35, 30][3] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_transparent_dispose():
|
@pytest.mark.parametrize(
|
||||||
expected_colors = [
|
"loading_strategy, expected_colors",
|
||||||
(2, 1, 2),
|
(
|
||||||
((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
|
(
|
||||||
((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
|
GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
|
||||||
]
|
(
|
||||||
with Image.open("Tests/images/transparent_dispose.gif") as img:
|
(2, 1, 2),
|
||||||
for frame in range(3):
|
((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
|
||||||
img.seek(frame)
|
((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
|
||||||
for x in range(3):
|
),
|
||||||
color = img.getpixel((x, 0))
|
),
|
||||||
assert color == expected_colors[frame][x]
|
(
|
||||||
|
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
|
||||||
|
(
|
||||||
|
(2, 1, 2),
|
||||||
|
(0, 1, 0),
|
||||||
|
(2, 1, 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_transparent_dispose(loading_strategy, expected_colors):
|
||||||
|
GifImagePlugin.LOADING_STRATEGY = loading_strategy
|
||||||
|
try:
|
||||||
|
with Image.open("Tests/images/transparent_dispose.gif") as img:
|
||||||
|
for frame in range(3):
|
||||||
|
img.seek(frame)
|
||||||
|
for x in range(3):
|
||||||
|
color = img.getpixel((x, 0))
|
||||||
|
assert color == expected_colors[frame][x]
|
||||||
|
finally:
|
||||||
|
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||||
|
|
||||||
|
|
||||||
def test_dispose_previous():
|
def test_dispose_previous():
|
||||||
|
@ -831,6 +896,17 @@ def test_rgb_transparency(tmp_path):
|
||||||
assert "transparency" not in reloaded.info
|
assert "transparency" not in reloaded.info
|
||||||
|
|
||||||
|
|
||||||
|
def test_rgba_transparency(tmp_path):
|
||||||
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
|
im = hopper("P")
|
||||||
|
im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])
|
||||||
|
|
||||||
|
with Image.open(out) as reloaded:
|
||||||
|
reloaded.seek(1)
|
||||||
|
assert_image_equal(hopper("P").convert("RGB"), reloaded)
|
||||||
|
|
||||||
|
|
||||||
def test_bbox(tmp_path):
|
def test_bbox(tmp_path):
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
|
@ -960,6 +1036,11 @@ def test_lzw_bits():
|
||||||
def test_extents():
|
def test_extents():
|
||||||
with Image.open("Tests/images/test_extents.gif") as im:
|
with Image.open("Tests/images/test_extents.gif") as im:
|
||||||
assert im.size == (100, 100)
|
assert im.size == (100, 100)
|
||||||
|
|
||||||
|
# Check that n_frames does not change the size
|
||||||
|
assert im.n_frames == 2
|
||||||
|
assert im.size == (100, 100)
|
||||||
|
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
assert im.size == (150, 150)
|
assert im.size == (150, 150)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import io
|
import io
|
||||||
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -70,6 +71,53 @@ def test_save_to_bytes():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_duplicates(tmp_path):
|
||||||
|
temp_file = str(tmp_path / "temp.ico")
|
||||||
|
temp_file2 = str(tmp_path / "temp2.ico")
|
||||||
|
|
||||||
|
im = hopper()
|
||||||
|
sizes = [(32, 32), (64, 64)]
|
||||||
|
im.save(temp_file, "ico", sizes=sizes)
|
||||||
|
|
||||||
|
sizes.append(sizes[-1])
|
||||||
|
im.save(temp_file2, "ico", sizes=sizes)
|
||||||
|
|
||||||
|
assert os.path.getsize(temp_file) == os.path.getsize(temp_file2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_different_bit_depths(tmp_path):
|
||||||
|
temp_file = str(tmp_path / "temp.ico")
|
||||||
|
temp_file2 = str(tmp_path / "temp2.ico")
|
||||||
|
|
||||||
|
im = hopper()
|
||||||
|
im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)])
|
||||||
|
|
||||||
|
hopper("1").save(
|
||||||
|
temp_file2,
|
||||||
|
"ico",
|
||||||
|
bitmap_format="bmp",
|
||||||
|
sizes=[(128, 128)],
|
||||||
|
append_images=[im],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert os.path.getsize(temp_file) != os.path.getsize(temp_file2)
|
||||||
|
|
||||||
|
# Test that only matching sizes of different bit depths are saved
|
||||||
|
temp_file3 = str(tmp_path / "temp3.ico")
|
||||||
|
temp_file4 = str(tmp_path / "temp4.ico")
|
||||||
|
|
||||||
|
im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)])
|
||||||
|
im.save(
|
||||||
|
temp_file4,
|
||||||
|
"ico",
|
||||||
|
bitmap_format="bmp",
|
||||||
|
sizes=[(128, 128)],
|
||||||
|
append_images=[Image.new("P", (64, 64))],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert os.path.getsize(temp_file3) == os.path.getsize(temp_file4)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
|
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
|
||||||
def test_save_to_bytes_bmp(mode):
|
def test_save_to_bytes_bmp(mode):
|
||||||
output = io.BytesIO()
|
output = io.BytesIO()
|
||||||
|
|
|
@ -68,6 +68,13 @@ class TestFileJpeg:
|
||||||
assert im.format == "JPEG"
|
assert im.format == "JPEG"
|
||||||
assert im.get_format_mimetype() == "image/jpeg"
|
assert im.get_format_mimetype() == "image/jpeg"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||||
|
def test_zero(self, size, tmp_path):
|
||||||
|
f = str(tmp_path / "temp.jpg")
|
||||||
|
im = Image.new("RGB", size)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.save(f)
|
||||||
|
|
||||||
def test_app(self):
|
def test_app(self):
|
||||||
# Test APP/COM reader (@PIL135)
|
# Test APP/COM reader (@PIL135)
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
|
|
|
@ -209,6 +209,49 @@ def test_layers():
|
||||||
assert_image_similar(im, test_card, 0.4)
|
assert_image_similar(im, test_card, 0.4)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"name, args, offset, data",
|
||||||
|
(
|
||||||
|
("foo.j2k", {}, 0, b"\xff\x4f"),
|
||||||
|
("foo.jp2", {}, 4, b"jP"),
|
||||||
|
(None, {"no_jp2": True}, 0, b"\xff\x4f"),
|
||||||
|
("foo.j2k", {"no_jp2": True}, 0, b"\xff\x4f"),
|
||||||
|
("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"),
|
||||||
|
("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"),
|
||||||
|
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
|
||||||
|
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_no_jp2(name, args, offset, data):
|
||||||
|
out = BytesIO()
|
||||||
|
if name:
|
||||||
|
out.name = name
|
||||||
|
test_card.save(out, "JPEG2000", **args)
|
||||||
|
out.seek(offset)
|
||||||
|
assert out.read(2) == data
|
||||||
|
|
||||||
|
|
||||||
|
def test_mct():
|
||||||
|
# Three component
|
||||||
|
for val in (0, 1):
|
||||||
|
out = BytesIO()
|
||||||
|
test_card.save(out, "JPEG2000", mct=val, no_jp2=True)
|
||||||
|
|
||||||
|
assert out.getvalue()[59] == val
|
||||||
|
with Image.open(out) as im:
|
||||||
|
assert_image_similar(im, test_card, 1.0e-3)
|
||||||
|
|
||||||
|
# Single component should have MCT disabled
|
||||||
|
for val in (0, 1):
|
||||||
|
out = BytesIO()
|
||||||
|
with Image.open("Tests/images/16bit.cropped.jp2") as jp2:
|
||||||
|
jp2.save(out, "JPEG2000", mct=val, no_jp2=True)
|
||||||
|
|
||||||
|
assert out.getvalue()[53] == 0
|
||||||
|
with Image.open(out) as im:
|
||||||
|
assert_image_similar(im, jp2, 1.0e-3)
|
||||||
|
|
||||||
|
|
||||||
def test_rgba():
|
def test_rgba():
|
||||||
# Arrange
|
# Arrange
|
||||||
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
|
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
|
||||||
|
|
|
@ -4,7 +4,6 @@ import itertools
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from ctypes import c_float
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -168,14 +167,11 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
val = original[tag]
|
val = original[tag]
|
||||||
if tag.endswith("Resolution"):
|
if tag.endswith("Resolution"):
|
||||||
if legacy_api:
|
if legacy_api:
|
||||||
assert (
|
assert val[0][0] / val[0][1] == (
|
||||||
c_float(val[0][0] / val[0][1]).value
|
4294967295 / 113653537
|
||||||
== c_float(value[0][0] / value[0][1]).value
|
|
||||||
), f"{tag} didn't roundtrip"
|
), f"{tag} didn't roundtrip"
|
||||||
else:
|
else:
|
||||||
assert (
|
assert val == 37.79000115940079, f"{tag} didn't roundtrip"
|
||||||
c_float(val).value == c_float(value).value
|
|
||||||
), f"{tag} didn't roundtrip"
|
|
||||||
else:
|
else:
|
||||||
assert val == value, f"{tag} didn't roundtrip"
|
assert val == value, f"{tag} didn't roundtrip"
|
||||||
|
|
||||||
|
@ -218,7 +214,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
values = {
|
values = {
|
||||||
2: "test",
|
2: "test",
|
||||||
3: 1,
|
3: 1,
|
||||||
4: 2 ** 20,
|
4: 2**20,
|
||||||
5: TiffImagePlugin.IFDRational(100, 1),
|
5: TiffImagePlugin.IFDRational(100, 1),
|
||||||
12: 1.05,
|
12: 1.05,
|
||||||
}
|
}
|
||||||
|
@ -1019,7 +1015,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
im = hopper("RGB").resize((256, 256))
|
im = hopper("RGB").resize((256, 256))
|
||||||
out = str(tmp_path / "temp.tif")
|
out = str(tmp_path / "temp.tif")
|
||||||
|
|
||||||
TiffImagePlugin.STRIP_SIZE = 2 ** 18
|
TiffImagePlugin.STRIP_SIZE = 2**18
|
||||||
try:
|
try:
|
||||||
|
|
||||||
im.save(out, compression="tiff_adobe_deflate")
|
im.save(out, compression="tiff_adobe_deflate")
|
||||||
|
|
|
@ -13,16 +13,53 @@ TEST_FILE = "Tests/images/hopper.ppm"
|
||||||
|
|
||||||
def test_sanity():
|
def test_sanity():
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
im.load()
|
|
||||||
assert im.mode == "RGB"
|
assert im.mode == "RGB"
|
||||||
assert im.size == (128, 128)
|
assert im.size == (128, 128)
|
||||||
assert im.format, "PPM"
|
assert im.format == "PPM"
|
||||||
assert im.get_format_mimetype() == "image/x-portable-pixmap"
|
assert im.get_format_mimetype() == "image/x-portable-pixmap"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"data, mode, pixels",
|
||||||
|
(
|
||||||
|
(b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
|
||||||
|
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
|
||||||
|
# P6 with maxval < 255
|
||||||
|
(
|
||||||
|
b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11",
|
||||||
|
"RGB",
|
||||||
|
(
|
||||||
|
(0, 15, 30),
|
||||||
|
(120, 135, 150),
|
||||||
|
(225, 240, 255),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
# P6 with maxval > 255
|
||||||
|
# Scale down to 255, since there is no RGB mode with more than 8-bit
|
||||||
|
(
|
||||||
|
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
|
||||||
|
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
|
||||||
|
"RGB",
|
||||||
|
(
|
||||||
|
(0, 1, 2),
|
||||||
|
(127, 128, 129),
|
||||||
|
(254, 255, 255),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_arbitrary_maxval(data, mode, pixels):
|
||||||
|
fp = BytesIO(data)
|
||||||
|
with Image.open(fp) as im:
|
||||||
|
assert im.size == (3, 1)
|
||||||
|
assert im.mode == mode
|
||||||
|
|
||||||
|
px = im.load()
|
||||||
|
assert tuple(px[x, 0] for x in range(3)) == pixels
|
||||||
|
|
||||||
|
|
||||||
def test_16bit_pgm():
|
def test_16bit_pgm():
|
||||||
with Image.open("Tests/images/16_bit_binary.pgm") as im:
|
with Image.open("Tests/images/16_bit_binary.pgm") as im:
|
||||||
im.load()
|
|
||||||
assert im.mode == "I"
|
assert im.mode == "I"
|
||||||
assert im.size == (20, 100)
|
assert im.size == (20, 100)
|
||||||
assert im.get_format_mimetype() == "image/x-portable-graymap"
|
assert im.get_format_mimetype() == "image/x-portable-graymap"
|
||||||
|
@ -32,8 +69,6 @@ def test_16bit_pgm():
|
||||||
|
|
||||||
def test_16bit_pgm_write(tmp_path):
|
def test_16bit_pgm_write(tmp_path):
|
||||||
with Image.open("Tests/images/16_bit_binary.pgm") as im:
|
with Image.open("Tests/images/16_bit_binary.pgm") as im:
|
||||||
im.load()
|
|
||||||
|
|
||||||
f = str(tmp_path / "temp.pgm")
|
f = str(tmp_path / "temp.pgm")
|
||||||
im.save(f, "PPM")
|
im.save(f, "PPM")
|
||||||
|
|
||||||
|
@ -91,19 +126,8 @@ def test_token_too_long(tmp_path):
|
||||||
assert str(e.value) == "Token too long in file header: b'01234567890'"
|
assert str(e.value) == "Token too long in file header: b'01234567890'"
|
||||||
|
|
||||||
|
|
||||||
def test_too_many_colors(tmp_path):
|
|
||||||
path = str(tmp_path / "temp.ppm")
|
|
||||||
with open(path, "wb") as f:
|
|
||||||
f.write(b"P6\n1 1\n1000\n")
|
|
||||||
|
|
||||||
with pytest.raises(ValueError) as e:
|
|
||||||
with Image.open(path):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert str(e.value) == "Too many colors for band: 1000"
|
|
||||||
|
|
||||||
|
|
||||||
def test_truncated_file(tmp_path):
|
def test_truncated_file(tmp_path):
|
||||||
|
# Test EOF in header
|
||||||
path = str(tmp_path / "temp.pgm")
|
path = str(tmp_path / "temp.pgm")
|
||||||
with open(path, "w") as f:
|
with open(path, "w") as f:
|
||||||
f.write("P6")
|
f.write("P6")
|
||||||
|
@ -114,6 +138,12 @@ def test_truncated_file(tmp_path):
|
||||||
|
|
||||||
assert str(e.value) == "Reached EOF while reading header"
|
assert str(e.value) == "Reached EOF while reading header"
|
||||||
|
|
||||||
|
# Test EOF for PyDecoder
|
||||||
|
fp = BytesIO(b"P5 3 1 4")
|
||||||
|
with Image.open(fp) as im:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_neg_ppm():
|
def test_neg_ppm():
|
||||||
# Storage.c accepted negative values for xsize, ysize. the
|
# Storage.c accepted negative values for xsize, ysize. the
|
||||||
|
|
|
@ -87,6 +87,10 @@ class TestFileTiff:
|
||||||
|
|
||||||
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
|
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
|
||||||
|
|
||||||
|
def test_bigtiff(self):
|
||||||
|
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"file_name,mode,size,offset",
|
"file_name,mode,size,offset",
|
||||||
[
|
[
|
||||||
|
@ -221,6 +225,15 @@ class TestFileTiff:
|
||||||
assert b[0] == ord(b"\x01")
|
assert b[0] == ord(b"\x01")
|
||||||
assert b[1] == ord(b"\xe0")
|
assert b[1] == ord(b"\xe0")
|
||||||
|
|
||||||
|
def test_16bit_r(self):
|
||||||
|
with Image.open("Tests/images/16bit.r.tif") as im:
|
||||||
|
assert im.getpixel((0, 0)) == 480
|
||||||
|
assert im.mode == "I;16"
|
||||||
|
|
||||||
|
b = im.tobytes()
|
||||||
|
assert b[0] == ord(b"\xe0")
|
||||||
|
assert b[1] == ord(b"\x01")
|
||||||
|
|
||||||
def test_16bit_s(self):
|
def test_16bit_s(self):
|
||||||
with Image.open("Tests/images/16bit.s.tif") as im:
|
with Image.open("Tests/images/16bit.s.tif") as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
@ -598,6 +611,17 @@ class TestFileTiff:
|
||||||
with Image.open(infile) as im:
|
with Image.open(infile) as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
|
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
|
||||||
|
|
||||||
|
def test_planar_configuration_save(self, tmp_path):
|
||||||
|
infile = "Tests/images/tiff_tiled_planar_raw.tif"
|
||||||
|
with Image.open(infile) as im:
|
||||||
|
assert im._planar_configuration == 2
|
||||||
|
|
||||||
|
outfile = str(tmp_path / "temp.tif")
|
||||||
|
im.save(outfile)
|
||||||
|
|
||||||
|
with Image.open(outfile) as reloaded:
|
||||||
|
assert_image_equal_tofile(reloaded, infile)
|
||||||
|
|
||||||
def test_palette(self, tmp_path):
|
def test_palette(self, tmp_path):
|
||||||
def roundtrip(mode):
|
def roundtrip(mode):
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = str(tmp_path / "temp.tif")
|
||||||
|
|
|
@ -258,7 +258,7 @@ def test_ifd_unsigned_rational(tmp_path):
|
||||||
im = hopper()
|
im = hopper()
|
||||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
|
||||||
max_long = 2 ** 32 - 1
|
max_long = 2**32 - 1
|
||||||
|
|
||||||
# 4 bytes unsigned long
|
# 4 bytes unsigned long
|
||||||
numerator = max_long
|
numerator = max_long
|
||||||
|
@ -290,8 +290,8 @@ def test_ifd_signed_rational(tmp_path):
|
||||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
|
||||||
# pair of 4 byte signed longs
|
# pair of 4 byte signed longs
|
||||||
numerator = 2 ** 31 - 1
|
numerator = 2**31 - 1
|
||||||
denominator = -(2 ** 31)
|
denominator = -(2**31)
|
||||||
|
|
||||||
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
||||||
|
|
||||||
|
@ -302,8 +302,8 @@ def test_ifd_signed_rational(tmp_path):
|
||||||
assert numerator == reloaded.tag_v2[37380].numerator
|
assert numerator == reloaded.tag_v2[37380].numerator
|
||||||
assert denominator == reloaded.tag_v2[37380].denominator
|
assert denominator == reloaded.tag_v2[37380].denominator
|
||||||
|
|
||||||
numerator = -(2 ** 31)
|
numerator = -(2**31)
|
||||||
denominator = 2 ** 31 - 1
|
denominator = 2**31 - 1
|
||||||
|
|
||||||
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
||||||
|
|
||||||
|
@ -315,7 +315,7 @@ def test_ifd_signed_rational(tmp_path):
|
||||||
assert denominator == reloaded.tag_v2[37380].denominator
|
assert denominator == reloaded.tag_v2[37380].denominator
|
||||||
|
|
||||||
# out of bounds of 4 byte signed long
|
# out of bounds of 4 byte signed long
|
||||||
numerator = -(2 ** 31) - 1
|
numerator = -(2**31) - 1
|
||||||
denominator = 1
|
denominator = 1
|
||||||
|
|
||||||
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
||||||
|
@ -324,7 +324,7 @@ def test_ifd_signed_rational(tmp_path):
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
assert 2 ** 31 - 1 == reloaded.tag_v2[37380].numerator
|
assert 2**31 - 1 == reloaded.tag_v2[37380].numerator
|
||||||
assert -1 == reloaded.tag_v2[37380].denominator
|
assert -1 == reloaded.tag_v2[37380].denominator
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import pytest
|
||||||
from PIL import Image, WebPImagePlugin, features
|
from PIL import Image, WebPImagePlugin, features
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
|
assert_image_equal,
|
||||||
assert_image_similar,
|
assert_image_similar,
|
||||||
assert_image_similar_tofile,
|
assert_image_similar_tofile,
|
||||||
hopper,
|
hopper,
|
||||||
|
@ -105,6 +106,19 @@ class TestFileWebp:
|
||||||
hopper().save(buffer_method, format="WEBP", method=6)
|
hopper().save(buffer_method, format="WEBP", method=6)
|
||||||
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
|
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
|
||||||
|
|
||||||
|
@skip_unless_feature("webp_anim")
|
||||||
|
def test_save_all(self, tmp_path):
|
||||||
|
temp_file = str(tmp_path / "temp.webp")
|
||||||
|
im = Image.new("RGB", (1, 1))
|
||||||
|
im2 = Image.new("RGB", (1, 1), "#f00")
|
||||||
|
im.save(temp_file, save_all=True, append_images=[im2])
|
||||||
|
|
||||||
|
with Image.open(temp_file) as reloaded:
|
||||||
|
assert_image_equal(im, reloaded)
|
||||||
|
|
||||||
|
reloaded.seek(1)
|
||||||
|
assert_image_similar(im2, reloaded, 1)
|
||||||
|
|
||||||
def test_icc_profile(self, tmp_path):
|
def test_icc_profile(self, tmp_path):
|
||||||
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
|
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
|
||||||
if _webp.HAVE_WEBPANIM:
|
if _webp.HAVE_WEBPANIM:
|
||||||
|
@ -128,7 +142,7 @@ class TestFileWebp:
|
||||||
|
|
||||||
self._roundtrip(tmp_path, "P", 50.0)
|
self._roundtrip(tmp_path, "P", 50.0)
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system")
|
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
|
||||||
def test_write_encoding_error_message(self, tmp_path):
|
def test_write_encoding_error_message(self, tmp_path):
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = str(tmp_path / "temp.webp")
|
||||||
im = Image.new("RGB", (15000, 15000))
|
im = Image.new("RGB", (15000, 15000))
|
||||||
|
@ -171,9 +185,14 @@ class TestFileWebp:
|
||||||
Image.open(blob).load()
|
Image.open(blob).load()
|
||||||
Image.open(blob).load()
|
Image.open(blob).load()
|
||||||
|
|
||||||
@skip_unless_feature("webp")
|
|
||||||
@skip_unless_feature("webp_anim")
|
@skip_unless_feature("webp_anim")
|
||||||
def test_background_from_gif(self, tmp_path):
|
def test_background_from_gif(self, tmp_path):
|
||||||
|
# Save L mode GIF with background
|
||||||
|
with Image.open("Tests/images/no_palette_with_background.gif") as im:
|
||||||
|
out_webp = str(tmp_path / "temp.webp")
|
||||||
|
im.save(out_webp, save_all=True)
|
||||||
|
|
||||||
|
# Save P mode GIF with background
|
||||||
with Image.open("Tests/images/chi.gif") as im:
|
with Image.open("Tests/images/chi.gif") as im:
|
||||||
original_value = im.convert("RGB").getpixel((1, 1))
|
original_value = im.convert("RGB").getpixel((1, 1))
|
||||||
|
|
||||||
|
@ -191,7 +210,6 @@ class TestFileWebp:
|
||||||
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
|
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
|
||||||
assert difference < 5
|
assert difference < 5
|
||||||
|
|
||||||
@skip_unless_feature("webp")
|
|
||||||
@skip_unless_feature("webp_anim")
|
@skip_unless_feature("webp_anim")
|
||||||
def test_duration(self, tmp_path):
|
def test_duration(self, tmp_path):
|
||||||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from packaging.version import parse as parse_version
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image, features
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
|
@ -27,7 +28,6 @@ def test_n_frames():
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
|
|
||||||
def test_write_animation_L(tmp_path):
|
def test_write_animation_L(tmp_path):
|
||||||
"""
|
"""
|
||||||
Convert an animated GIF to animated WebP, then compare the frame count, and first
|
Convert an animated GIF to animated WebP, then compare the frame count, and first
|
||||||
|
@ -46,6 +46,11 @@ def test_write_animation_L(tmp_path):
|
||||||
orig.load()
|
orig.load()
|
||||||
im.load()
|
im.load()
|
||||||
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
||||||
|
|
||||||
|
if is_big_endian():
|
||||||
|
webp = parse_version(features.version_module("webp"))
|
||||||
|
if webp < parse_version("1.2.2"):
|
||||||
|
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||||
orig.seek(orig.n_frames - 1)
|
orig.seek(orig.n_frames - 1)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
orig.load()
|
orig.load()
|
||||||
|
@ -53,7 +58,6 @@ def test_write_animation_L(tmp_path):
|
||||||
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
|
|
||||||
def test_write_animation_RGB(tmp_path):
|
def test_write_animation_RGB(tmp_path):
|
||||||
"""
|
"""
|
||||||
Write an animated WebP from RGB frames, and ensure the frames
|
Write an animated WebP from RGB frames, and ensure the frames
|
||||||
|
@ -69,6 +73,10 @@ def test_write_animation_RGB(tmp_path):
|
||||||
assert_image_equal(im, frame1.convert("RGBA"))
|
assert_image_equal(im, frame1.convert("RGBA"))
|
||||||
|
|
||||||
# Compare second frame to original
|
# Compare second frame to original
|
||||||
|
if is_big_endian():
|
||||||
|
webp = parse_version(features.version_module("webp"))
|
||||||
|
if webp < parse_version("1.2.2"):
|
||||||
|
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
im.load()
|
im.load()
|
||||||
assert_image_equal(im, frame2.convert("RGBA"))
|
assert_image_equal(im, frame2.convert("RGBA"))
|
||||||
|
|
|
@ -2,7 +2,7 @@ from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image, XbmImagePlugin
|
||||||
|
|
||||||
from .helper import hopper
|
from .helper import hopper
|
||||||
|
|
||||||
|
@ -63,6 +63,13 @@ def test_open_filename_with_underscore():
|
||||||
assert im.size == (128, 128)
|
assert im.size == (128, 128)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_file():
|
||||||
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
|
with pytest.raises(SyntaxError):
|
||||||
|
XbmImagePlugin.XbmImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
def test_save_wrong_mode(tmp_path):
|
def test_save_wrong_mode(tmp_path):
|
||||||
im = hopper()
|
im = hopper()
|
||||||
out = str(tmp_path / "temp.xbm")
|
out = str(tmp_path / "temp.xbm")
|
||||||
|
|
|
@ -652,6 +652,15 @@ class TestImage:
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
|
|
||||||
|
def test_no_new_file_on_error(self, tmp_path):
|
||||||
|
temp_file = str(tmp_path / "temp.jpg")
|
||||||
|
|
||||||
|
im = Image.new("RGB", (0, 0))
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.save(temp_file)
|
||||||
|
|
||||||
|
assert not os.path.exists(temp_file)
|
||||||
|
|
||||||
def test_load_on_nonexclusive_multiframe(self):
|
def test_load_on_nonexclusive_multiframe(self):
|
||||||
with open("Tests/images/frozenpond.mpo", "rb") as fp:
|
with open("Tests/images/frozenpond.mpo", "rb") as fp:
|
||||||
|
|
||||||
|
@ -666,6 +675,19 @@ class TestImage:
|
||||||
|
|
||||||
assert not fp.closed
|
assert not fp.closed
|
||||||
|
|
||||||
|
def test_empty_exif(self):
|
||||||
|
with Image.open("Tests/images/exif.png") as im:
|
||||||
|
exif = im.getexif()
|
||||||
|
assert dict(exif) != {}
|
||||||
|
|
||||||
|
# Test that exif data is cleared after another load
|
||||||
|
exif.load(None)
|
||||||
|
assert dict(exif) == {}
|
||||||
|
|
||||||
|
# Test loading just the EXIF header
|
||||||
|
exif.load(b"Exif\x00\x00")
|
||||||
|
assert dict(exif) == {}
|
||||||
|
|
||||||
@mark_if_feature_version(
|
@mark_if_feature_version(
|
||||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import ctypes
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
@ -154,14 +153,17 @@ class TestImageGetPixel(AccessTest):
|
||||||
|
|
||||||
# Check 0
|
# Check 0
|
||||||
im = Image.new(mode, (0, 0), None)
|
im = Image.new(mode, (0, 0), None)
|
||||||
with pytest.raises(IndexError):
|
assert im.load() is not None
|
||||||
|
|
||||||
|
error = ValueError if self._need_cffi_access else IndexError
|
||||||
|
with pytest.raises(error):
|
||||||
im.putpixel((0, 0), c)
|
im.putpixel((0, 0), c)
|
||||||
with pytest.raises(IndexError):
|
with pytest.raises(error):
|
||||||
im.getpixel((0, 0))
|
im.getpixel((0, 0))
|
||||||
# Check 0 negative index
|
# Check 0 negative index
|
||||||
with pytest.raises(IndexError):
|
with pytest.raises(error):
|
||||||
im.putpixel((-1, -1), c)
|
im.putpixel((-1, -1), c)
|
||||||
with pytest.raises(IndexError):
|
with pytest.raises(error):
|
||||||
im.getpixel((-1, -1))
|
im.getpixel((-1, -1))
|
||||||
|
|
||||||
# check initial color
|
# check initial color
|
||||||
|
@ -176,10 +178,10 @@ class TestImageGetPixel(AccessTest):
|
||||||
|
|
||||||
# Check 0
|
# Check 0
|
||||||
im = Image.new(mode, (0, 0), c)
|
im = Image.new(mode, (0, 0), c)
|
||||||
with pytest.raises(IndexError):
|
with pytest.raises(error):
|
||||||
im.getpixel((0, 0))
|
im.getpixel((0, 0))
|
||||||
# Check 0 negative index
|
# Check 0 negative index
|
||||||
with pytest.raises(IndexError):
|
with pytest.raises(error):
|
||||||
im.getpixel((-1, -1))
|
im.getpixel((-1, -1))
|
||||||
|
|
||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
|
@ -205,10 +207,10 @@ class TestImageGetPixel(AccessTest):
|
||||||
# see https://github.com/python-pillow/Pillow/issues/452
|
# see https://github.com/python-pillow/Pillow/issues/452
|
||||||
# pixelaccess is using signed int* instead of uint*
|
# pixelaccess is using signed int* instead of uint*
|
||||||
for mode in ("I;16", "I;16B"):
|
for mode in ("I;16", "I;16B"):
|
||||||
self.check(mode, 2 ** 15 - 1)
|
self.check(mode, 2**15 - 1)
|
||||||
self.check(mode, 2 ** 15)
|
self.check(mode, 2**15)
|
||||||
self.check(mode, 2 ** 15 + 1)
|
self.check(mode, 2**15 + 1)
|
||||||
self.check(mode, 2 ** 16 - 1)
|
self.check(mode, 2**16 - 1)
|
||||||
|
|
||||||
def test_p_putpixel_rgb_rgba(self):
|
def test_p_putpixel_rgb_rgba(self):
|
||||||
for color in [(255, 0, 0), (255, 0, 0, 255)]:
|
for color in [(255, 0, 0), (255, 0, 0, 255)]:
|
||||||
|
@ -386,7 +388,7 @@ class TestImagePutPixelError(AccessTest):
|
||||||
def test_putpixel_overflow_error(self, mode):
|
def test_putpixel_overflow_error(self, mode):
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
with pytest.raises(OverflowError):
|
with pytest.raises(OverflowError):
|
||||||
im.putpixel((0, 0), 2 ** 80)
|
im.putpixel((0, 0), 2**80)
|
||||||
|
|
||||||
def test_putpixel_unrecognized_mode(self):
|
def test_putpixel_unrecognized_mode(self):
|
||||||
im = hopper("BGR;15")
|
im = hopper("BGR;15")
|
||||||
|
@ -401,6 +403,8 @@ class TestEmbeddable:
|
||||||
"not from shell",
|
"not from shell",
|
||||||
)
|
)
|
||||||
def test_embeddable(self):
|
def test_embeddable(self):
|
||||||
|
import ctypes
|
||||||
|
|
||||||
with open("embed_pil.c", "w") as fh:
|
with open("embed_pil.c", "w") as fh:
|
||||||
fh.write(
|
fh.write(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -70,6 +70,11 @@ def test_16bit():
|
||||||
with Image.open("Tests/images/16bit.cropped.tif") as im:
|
with Image.open("Tests/images/16bit.cropped.tif") as im:
|
||||||
_test_float_conversion(im)
|
_test_float_conversion(im)
|
||||||
|
|
||||||
|
for color in (65535, 65536):
|
||||||
|
im = Image.new("I", (1, 1), color)
|
||||||
|
im_i16 = im.convert("I;16")
|
||||||
|
assert im_i16.getpixel((0, 0)) == 65535
|
||||||
|
|
||||||
|
|
||||||
def test_16bit_workaround():
|
def test_16bit_workaround():
|
||||||
with Image.open("Tests/images/16bit.cropped.tif") as im:
|
with Image.open("Tests/images/16bit.cropped.tif") as im:
|
||||||
|
@ -135,6 +140,10 @@ def test_trns_l(tmp_path):
|
||||||
|
|
||||||
f = str(tmp_path / "temp.png")
|
f = str(tmp_path / "temp.png")
|
||||||
|
|
||||||
|
im_la = im.convert("LA")
|
||||||
|
assert "transparency" not in im_la.info
|
||||||
|
im_la.save(f)
|
||||||
|
|
||||||
im_rgb = im.convert("RGB")
|
im_rgb = im.convert("RGB")
|
||||||
assert im_rgb.info["transparency"] == (128, 128, 128) # undone
|
assert im_rgb.info["transparency"] == (128, 128, 128) # undone
|
||||||
im_rgb.save(f)
|
im_rgb.save(f)
|
||||||
|
|
|
@ -67,6 +67,16 @@ class TestImagingPaste:
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def gradient_LA(self):
|
||||||
|
return Image.merge(
|
||||||
|
"LA",
|
||||||
|
[
|
||||||
|
self.gradient_L,
|
||||||
|
self.gradient_L.transpose(Image.Transpose.ROTATE_90),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def gradient_RGBA(self):
|
def gradient_RGBA(self):
|
||||||
return Image.merge(
|
return Image.merge(
|
||||||
|
@ -145,6 +155,28 @@ class TestImagingPaste:
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_image_mask_LA(self):
|
||||||
|
for mode in ("RGBA", "RGB", "L"):
|
||||||
|
im = Image.new(mode, (200, 200), "white")
|
||||||
|
im2 = getattr(self, "gradient_" + mode)
|
||||||
|
|
||||||
|
self.assert_9points_paste(
|
||||||
|
im,
|
||||||
|
im2,
|
||||||
|
self.gradient_LA,
|
||||||
|
[
|
||||||
|
(128, 191, 255, 191),
|
||||||
|
(112, 207, 206, 111),
|
||||||
|
(128, 254, 128, 1),
|
||||||
|
(208, 208, 239, 239),
|
||||||
|
(192, 191, 191, 191),
|
||||||
|
(207, 207, 112, 113),
|
||||||
|
(255, 255, 255, 255),
|
||||||
|
(239, 207, 207, 239),
|
||||||
|
(255, 191, 128, 191),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_image_mask_RGBA(self):
|
def test_image_mask_RGBA(self):
|
||||||
for mode in ("RGBA", "RGB", "L"):
|
for mode in ("RGBA", "RGB", "L"):
|
||||||
im = Image.new(mode, (200, 200), "white")
|
im = Image.new(mode, (200, 200), "white")
|
||||||
|
|
|
@ -10,6 +10,7 @@ def test_sanity():
|
||||||
im.point(list(range(256)))
|
im.point(list(range(256)))
|
||||||
im.point(list(range(256)) * 3)
|
im.point(list(range(256)) * 3)
|
||||||
im.point(lambda x: x)
|
im.point(lambda x: x)
|
||||||
|
im.point(lambda x: x * 1.2)
|
||||||
|
|
||||||
im = im.convert("I")
|
im = im.convert("I")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
|
|
@ -38,7 +38,7 @@ def test_long_integers():
|
||||||
assert put(0xFFFFFFFF) == (255, 255, 255, 255)
|
assert put(0xFFFFFFFF) == (255, 255, 255, 255)
|
||||||
assert put(-1) == (255, 255, 255, 255)
|
assert put(-1) == (255, 255, 255, 255)
|
||||||
assert put(-1) == (255, 255, 255, 255)
|
assert put(-1) == (255, 255, 255, 255)
|
||||||
if sys.maxsize > 2 ** 32:
|
if sys.maxsize > 2**32:
|
||||||
assert put(sys.maxsize) == (255, 255, 255, 255)
|
assert put(sys.maxsize) == (255, 255, 255, 255)
|
||||||
else:
|
else:
|
||||||
assert put(sys.maxsize) == (255, 255, 255, 127)
|
assert put(sys.maxsize) == (255, 255, 255, 127)
|
||||||
|
|
|
@ -264,6 +264,13 @@ class TestImageResize:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.resize((10, 10), "unknown")
|
im.resize((10, 10), "unknown")
|
||||||
|
|
||||||
|
def test_load_first(self):
|
||||||
|
# load() may change the size of the image
|
||||||
|
# Test that resize() is calling it before getting the size
|
||||||
|
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||||
|
im = im.resize((64, 64))
|
||||||
|
assert im.size == (64, 64)
|
||||||
|
|
||||||
def test_default_filter(self):
|
def test_default_filter(self):
|
||||||
for mode in "L", "RGB", "I", "F":
|
for mode in "L", "RGB", "I", "F":
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
|
|
|
@ -88,6 +88,14 @@ def test_no_resize():
|
||||||
assert im.size == (64, 64)
|
assert im.size == (64, 64)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_first():
|
||||||
|
# load() may change the size of the image
|
||||||
|
# Test that thumbnail() is calling it before performing size calculations
|
||||||
|
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||||
|
im.thumbnail((64, 64))
|
||||||
|
assert im.size == (64, 10)
|
||||||
|
|
||||||
|
|
||||||
# valgrind test is failing with memory allocated in libjpeg
|
# valgrind test is failing with memory allocated in libjpeg
|
||||||
@pytest.mark.valgrind_known_error(reason="Known Failing")
|
@pytest.mark.valgrind_known_error(reason="Known Failing")
|
||||||
def test_DCT_scaling_edges():
|
def test_DCT_scaling_edges():
|
||||||
|
@ -130,4 +138,4 @@ def test_reducing_gap_for_DCT_scaling():
|
||||||
with Image.open("Tests/images/hopper.jpg") as im:
|
with Image.open("Tests/images/hopper.jpg") as im:
|
||||||
im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
|
im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
|
||||||
|
|
||||||
assert_image_equal(ref, im)
|
assert_image_similar(ref, im, 1.4)
|
||||||
|
|
|
@ -303,7 +303,7 @@ def test_extended_information():
|
||||||
def assert_truncated_tuple_equal(tup1, tup2, digits=10):
|
def assert_truncated_tuple_equal(tup1, tup2, digits=10):
|
||||||
# Helper function to reduce precision of tuples of floats
|
# Helper function to reduce precision of tuples of floats
|
||||||
# recursively and then check equality.
|
# recursively and then check equality.
|
||||||
power = 10 ** digits
|
power = 10**digits
|
||||||
|
|
||||||
def truncate_tuple(tuple_or_float):
|
def truncate_tuple(tuple_or_float):
|
||||||
return tuple(
|
return tuple(
|
||||||
|
|
|
@ -1440,3 +1440,15 @@ def test_continuous_horizontal_edges_polygon():
|
||||||
assert_image_equal_tofile(
|
assert_image_equal_tofile(
|
||||||
img, expected, "continuous horizontal edges polygon failed"
|
img, expected, "continuous horizontal edges polygon failed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_discontiguous_corners_polygon():
|
||||||
|
img, draw = create_base_image_draw((84, 68))
|
||||||
|
draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK)
|
||||||
|
draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK)
|
||||||
|
draw.polygon(
|
||||||
|
((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)),
|
||||||
|
BLACK,
|
||||||
|
)
|
||||||
|
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
|
||||||
|
assert_image_similar_tofile(img, expected, 1)
|
||||||
|
|
|
@ -200,6 +200,9 @@ class MockPyEncoder(ImageFile.PyEncoder):
|
||||||
def encode(self, buffer):
|
def encode(self, buffer):
|
||||||
return 1, 1, b""
|
return 1, 1, b""
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
self.cleanup_called = True
|
||||||
|
|
||||||
|
|
||||||
xoff, yoff, xsize, ysize = 10, 20, 100, 100
|
xoff, yoff, xsize, ysize = 10, 20, 100, 100
|
||||||
|
|
||||||
|
@ -327,10 +330,12 @@ class TestPyEncoder(CodecsTest):
|
||||||
im = MockImageFile(buf)
|
im = MockImageFile(buf)
|
||||||
|
|
||||||
fp = BytesIO()
|
fp = BytesIO()
|
||||||
|
self.encoder.cleanup_called = False
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ImageFile._save(
|
ImageFile._save(
|
||||||
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
|
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
|
||||||
)
|
)
|
||||||
|
assert self.encoder.cleanup_called
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ImageFile._save(
|
ImageFile._save(
|
||||||
|
|
|
@ -48,10 +48,6 @@ def img_string_normalize(im):
|
||||||
return img_to_string(string_to_img(im))
|
return img_to_string(string_to_img(im))
|
||||||
|
|
||||||
|
|
||||||
def assert_img_equal(A, B):
|
|
||||||
assert img_to_string(A) == img_to_string(B)
|
|
||||||
|
|
||||||
|
|
||||||
def assert_img_equal_img_string(A, Bstring):
|
def assert_img_equal_img_string(A, Bstring):
|
||||||
assert img_to_string(A) == img_string_normalize(Bstring)
|
assert img_to_string(A) == img_string_normalize(Bstring)
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,7 @@ def test_sanity():
|
||||||
ImageOps.grayscale(hopper("L"))
|
ImageOps.grayscale(hopper("L"))
|
||||||
ImageOps.grayscale(hopper("RGB"))
|
ImageOps.grayscale(hopper("RGB"))
|
||||||
|
|
||||||
|
ImageOps.invert(hopper("1"))
|
||||||
ImageOps.invert(hopper("L"))
|
ImageOps.invert(hopper("L"))
|
||||||
ImageOps.invert(hopper("RGB"))
|
ImageOps.invert(hopper("RGB"))
|
||||||
|
|
||||||
|
|
|
@ -32,10 +32,10 @@ def test_rgb():
|
||||||
|
|
||||||
def checkrgb(r, g, b):
|
def checkrgb(r, g, b):
|
||||||
val = ImageQt.rgb(r, g, b)
|
val = ImageQt.rgb(r, g, b)
|
||||||
val = val % 2 ** 24 # drop the alpha
|
val = val % 2**24 # drop the alpha
|
||||||
assert val >> 16 == r
|
assert val >> 16 == r
|
||||||
assert ((val >> 8) % 2 ** 8) == g
|
assert ((val >> 8) % 2**8) == g
|
||||||
assert val % 2 ** 8 == b
|
assert val % 2**8 == b
|
||||||
|
|
||||||
checkrgb(0, 0, 0)
|
checkrgb(0, 0, 0)
|
||||||
checkrgb(255, 0, 0)
|
checkrgb(255, 0, 0)
|
||||||
|
|
|
@ -51,8 +51,8 @@ def test_constant():
|
||||||
st = ImageStat.Stat(im)
|
st = ImageStat.Stat(im)
|
||||||
|
|
||||||
assert st.extrema[0] == (128, 128)
|
assert st.extrema[0] == (128, 128)
|
||||||
assert st.sum[0] == 128 ** 3
|
assert st.sum[0] == 128**3
|
||||||
assert st.sum2[0] == 128 ** 4
|
assert st.sum2[0] == 128**4
|
||||||
assert st.mean[0] == 128
|
assert st.mean[0] == 128
|
||||||
assert st.median[0] == 128
|
assert st.median[0] == 128
|
||||||
assert st.rms[0] == 128
|
assert st.rms[0] == 128
|
||||||
|
|
|
@ -75,8 +75,16 @@ def test_photoimage_blank():
|
||||||
assert im_tk.width() == 100
|
assert im_tk.width() == 100
|
||||||
assert im_tk.height() == 100
|
assert im_tk.height() == 100
|
||||||
|
|
||||||
# reloaded = ImageTk.getimage(im_tk)
|
im = Image.new(mode, (100, 100))
|
||||||
# assert_image_equal(reloaded, im)
|
reloaded = ImageTk.getimage(im_tk)
|
||||||
|
assert_image_equal(reloaded.convert(mode), im)
|
||||||
|
|
||||||
|
|
||||||
|
def test_box_deprecation():
|
||||||
|
im = hopper()
|
||||||
|
im_tk = ImageTk.PhotoImage(im)
|
||||||
|
with pytest.warns(DeprecationWarning):
|
||||||
|
im_tk.paste(im, (0, 0, 128, 128))
|
||||||
|
|
||||||
|
|
||||||
def test_bitmapimage():
|
def test_bitmapimage():
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import ctypes
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from PIL import Image, ImageWin
|
from PIL import Image, ImageWin
|
||||||
|
@ -8,6 +7,7 @@ from .helper import hopper, is_win32
|
||||||
# see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652
|
# see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652
|
||||||
|
|
||||||
if is_win32():
|
if is_win32():
|
||||||
|
import ctypes
|
||||||
import ctypes.wintypes
|
import ctypes.wintypes
|
||||||
|
|
||||||
class BITMAPFILEHEADER(ctypes.Structure):
|
class BITMAPFILEHEADER(ctypes.Structure):
|
||||||
|
|
|
@ -444,6 +444,8 @@ class TestLibUnpack:
|
||||||
self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0))
|
self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0))
|
||||||
self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16))
|
self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16))
|
||||||
self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15))
|
self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15))
|
||||||
|
self.assert_unpack("RGBA", "BGRA;16L", 8, (6, 4, 2, 8), (14, 12, 10, 16))
|
||||||
|
self.assert_unpack("RGBA", "BGRA;16B", 8, (5, 3, 1, 7), (13, 11, 9, 15))
|
||||||
self.assert_unpack(
|
self.assert_unpack(
|
||||||
"RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)
|
"RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)
|
||||||
)
|
)
|
||||||
|
|
|
@ -36,7 +36,7 @@ def test_tobytes():
|
||||||
Image.MAX_IMAGE_PIXELS = max_pixels
|
Image.MAX_IMAGE_PIXELS = max_pixels
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system")
|
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
|
||||||
def test_ysize():
|
def test_ysize():
|
||||||
numpy = pytest.importorskip("numpy", reason="NumPy not installed")
|
numpy = pytest.importorskip("numpy", reason="NumPy not installed")
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,6 @@ def test_pdf_repr():
|
||||||
assert pdf_repr(True) == b"true"
|
assert pdf_repr(True) == b"true"
|
||||||
assert pdf_repr(False) == b"false"
|
assert pdf_repr(False) == b"false"
|
||||||
assert pdf_repr(None) == b"null"
|
assert pdf_repr(None) == b"null"
|
||||||
assert pdf_repr(b"a)/b\\(c") == br"(a\)/b\\\(c)"
|
assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)"
|
||||||
assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]"
|
assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]"
|
||||||
assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>"
|
assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>"
|
||||||
|
|
15
docs/conf.py
|
@ -16,8 +16,6 @@
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
# sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
import sphinx_rtd_theme
|
|
||||||
|
|
||||||
import PIL
|
import PIL
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
@ -126,13 +124,15 @@ nitpicky = True
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
|
|
||||||
html_theme = "sphinx_rtd_theme"
|
html_theme = "furo"
|
||||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
# further. For a list of options available for each theme, see the
|
# further. For a list of options available for each theme, see the
|
||||||
# documentation.
|
# documentation.
|
||||||
# html_theme_options = {}
|
html_theme_options = {
|
||||||
|
"light_logo": "pillow-logo-dark-text.png",
|
||||||
|
"dark_logo": "pillow-logo.png",
|
||||||
|
}
|
||||||
|
|
||||||
# Add any paths that contain custom themes here, relative to this directory.
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
# html_theme_path = []
|
# html_theme_path = []
|
||||||
|
@ -146,7 +146,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
# of the sidebar.
|
# of the sidebar.
|
||||||
html_logo = "resources/pillow-logo.png"
|
# html_logo = "resources/pillow-logo.png"
|
||||||
|
|
||||||
# The name of an image file (within the static path) to use as favicon of the
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
|
@ -311,10 +311,7 @@ texinfo_documents = [
|
||||||
|
|
||||||
|
|
||||||
def setup(app):
|
def setup(app):
|
||||||
app.add_js_file("js/script.js")
|
|
||||||
app.add_css_file("css/styles.css")
|
|
||||||
app.add_css_file("css/dark.css")
|
app.add_css_file("css/dark.css")
|
||||||
app.add_css_file("css/light.css")
|
|
||||||
|
|
||||||
|
|
||||||
# GitHub repo for sphinx-issues
|
# GitHub repo for sphinx-issues
|
||||||
|
|
|
@ -69,7 +69,7 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
|
||||||
Constants
|
Constants
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
.. deprecated:: 9.2.0
|
.. deprecated:: 9.1.0
|
||||||
|
|
||||||
A number of constants have been deprecated and will be removed in Pillow 10.0.0
|
A number of constants have been deprecated and will be removed in Pillow 10.0.0
|
||||||
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
|
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
|
||||||
|
@ -142,6 +142,13 @@ The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be re
|
||||||
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
|
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
|
||||||
:mod:`~PIL.FitsImagePlugin` instead.
|
:mod:`~PIL.FitsImagePlugin` instead.
|
||||||
|
|
||||||
|
PhotoImage.paste box parameter
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. deprecated:: 9.2.0
|
||||||
|
|
||||||
|
The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01).
|
||||||
|
|
||||||
Removed features
|
Removed features
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|
|
@ -210,7 +210,9 @@ class DdsImageFile(ImageFile.ImageFile):
|
||||||
format_description = "DirectDraw Surface"
|
format_description = "DirectDraw Surface"
|
||||||
|
|
||||||
def _open(self):
|
def _open(self):
|
||||||
magic, header_size = struct.unpack("<II", self.fp.read(8))
|
if not _accept(self.fp.read(4)):
|
||||||
|
raise SyntaxError("not a DDS file")
|
||||||
|
(header_size,) = struct.unpack("<I", self.fp.read(4))
|
||||||
if header_size != 124:
|
if header_size != 124:
|
||||||
raise OSError(f"Unsupported header size {repr(header_size)}")
|
raise OSError(f"Unsupported header size {repr(header_size)}")
|
||||||
header_bytes = self.fp.read(header_size - 4)
|
header_bytes = self.fp.read(header_size - 4)
|
||||||
|
@ -251,7 +253,7 @@ class DXT1Decoder(ImageFile.PyDecoder):
|
||||||
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
|
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
|
||||||
except struct.error as e:
|
except struct.error as e:
|
||||||
raise OSError("Truncated DDS file") from e
|
raise OSError("Truncated DDS file") from e
|
||||||
return 0, 0
|
return -1, 0
|
||||||
|
|
||||||
|
|
||||||
class DXT5Decoder(ImageFile.PyDecoder):
|
class DXT5Decoder(ImageFile.PyDecoder):
|
||||||
|
@ -262,7 +264,7 @@ class DXT5Decoder(ImageFile.PyDecoder):
|
||||||
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
|
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
|
||||||
except struct.error as e:
|
except struct.error as e:
|
||||||
raise OSError("Truncated DDS file") from e
|
raise OSError("Truncated DDS file") from e
|
||||||
return 0, 0
|
return -1, 0
|
||||||
|
|
||||||
|
|
||||||
Image.register_decoder("DXT1", DXT1Decoder)
|
Image.register_decoder("DXT1", DXT1Decoder)
|
||||||
|
|
|
@ -24,8 +24,6 @@ attribute will be ``None``.
|
||||||
Fully supported formats
|
Fully supported formats
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
.. contents::
|
|
||||||
|
|
||||||
BLP
|
BLP
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
|
@ -44,8 +42,9 @@ BMP
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``,
|
Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``,
|
||||||
or ``RGB`` data. 16-colour images are read as ``P`` images. Run-length encoding
|
or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length encoding
|
||||||
is not supported.
|
is not supported. Support for reading 8-bit run-length encoding was added in Pillow
|
||||||
|
9.1.0.
|
||||||
|
|
||||||
The :py:meth:`~PIL.Image.open` method sets the following
|
The :py:meth:`~PIL.Image.open` method sets the following
|
||||||
:py:attr:`~PIL.Image.Image.info` properties:
|
:py:attr:`~PIL.Image.Image.info` properties:
|
||||||
|
@ -106,8 +105,34 @@ writes run-length encoded files in GIF87a by default, unless GIF89a features
|
||||||
are used or GIF89a is already in use.
|
are used or GIF89a is already in use.
|
||||||
|
|
||||||
GIF files are initially read as grayscale (``L``) or palette mode (``P``)
|
GIF files are initially read as grayscale (``L``) or palette mode (``P``)
|
||||||
images, but seeking to later frames in an image will change the mode to either
|
images. Seeking to later frames in a ``P`` image will change the image to
|
||||||
``RGB`` or ``RGBA``, depending on whether the first frame had transparency.
|
``RGB`` (or ``RGBA`` if the first frame had transparency).
|
||||||
|
|
||||||
|
``P`` mode images are changed to ``RGB`` because each frame of a GIF may contain
|
||||||
|
its own individual palette of up to 256 colors. When a new frame is placed onto a
|
||||||
|
previous frame, those colors may combine to exceed the ``P`` mode limit of 256
|
||||||
|
colors. Instead, the image is converted to ``RGB`` handle this.
|
||||||
|
|
||||||
|
If you would prefer the first ``P`` image frame to be ``RGB`` as well, so that
|
||||||
|
every ``P`` frame is converted to ``RGB`` or ``RGBA`` mode, there is a setting
|
||||||
|
available::
|
||||||
|
|
||||||
|
from PIL import GifImagePlugin
|
||||||
|
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
|
||||||
|
|
||||||
|
GIF frames do not always contain individual palettes however. If there is only
|
||||||
|
a global palette, then all of the colors can fit within ``P`` mode. If you would
|
||||||
|
prefer the frames to be kept as ``P`` in that case, there is also a setting
|
||||||
|
available::
|
||||||
|
|
||||||
|
from PIL import GifImagePlugin
|
||||||
|
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
|
||||||
|
|
||||||
|
To restore the default behavior, where ``P`` mode images are only converted to
|
||||||
|
``RGB`` or ``RGBA`` after the first frame::
|
||||||
|
|
||||||
|
from PIL import GifImagePlugin
|
||||||
|
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||||
|
|
||||||
The :py:meth:`~PIL.Image.open` method sets the following
|
The :py:meth:`~PIL.Image.open` method sets the following
|
||||||
:py:attr:`~PIL.Image.Image.info` properties:
|
:py:attr:`~PIL.Image.Image.info` properties:
|
||||||
|
@ -364,10 +389,12 @@ The :py:meth:`~PIL.Image.open` method may set the following
|
||||||
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
||||||
|
|
||||||
**quality**
|
**quality**
|
||||||
The image quality, on a scale from 0 (worst) to 95 (best). The default is
|
The image quality, on a scale from 0 (worst) to 95 (best), or the string
|
||||||
75. Values above 95 should be avoided; 100 disables portions of the JPEG
|
``keep``. The default is 75. Values above 95 should be avoided; 100 disables
|
||||||
compression algorithm, and results in large files with hardly any gain in
|
portions of the JPEG compression algorithm, and results in large files with
|
||||||
image quality.
|
hardly any gain in image quality. The value ``keep`` is only valid for JPEG
|
||||||
|
files and will retain the original image quality level, subsampling, and
|
||||||
|
qtables.
|
||||||
|
|
||||||
**optimize**
|
**optimize**
|
||||||
If present and true, indicates that the encoder should make an extra pass
|
If present and true, indicates that the encoder should make an extra pass
|
||||||
|
@ -475,9 +502,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
||||||
and must be greater than the code-block size.
|
and must be greater than the code-block size.
|
||||||
|
|
||||||
**irreversible**
|
**irreversible**
|
||||||
If ``True``, use the lossy Irreversible Color Transformation
|
If ``True``, use the lossy discrete waveform transformation DWT 9-7.
|
||||||
followed by DWT 9-7. Defaults to ``False``, which means to use the
|
Defaults to ``False``, which uses the lossless DWT 5-3.
|
||||||
Reversible Color Transformation with DWT 5-3.
|
|
||||||
|
**mct**
|
||||||
|
If ``1`` then enable multiple component transformation when encoding,
|
||||||
|
otherwise use ``0`` for no component transformation (default). If MCT is
|
||||||
|
enabled and ``irreversible`` is ``True`` then the Irreversible Color
|
||||||
|
Transformation will be applied, otherwise encoding will use the
|
||||||
|
Reversible Color Transformation. MCT works best with a ``mode`` of
|
||||||
|
``RGB`` and is only applicable when the image data has 3 components.
|
||||||
|
|
||||||
|
.. versionadded:: 9.1.0
|
||||||
|
|
||||||
**progression**
|
**progression**
|
||||||
Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``,
|
Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``,
|
||||||
|
@ -497,6 +533,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
||||||
for compliant 4K files, *at least one* of the dimensions must match
|
for compliant 4K files, *at least one* of the dimensions must match
|
||||||
4096 x 2160.
|
4096 x 2160.
|
||||||
|
|
||||||
|
**no_jp2**
|
||||||
|
If ``True`` then don't wrap the raw codestream in the JP2 file format when
|
||||||
|
saving, otherwise the extension of the filename will be used to determine
|
||||||
|
the format (default).
|
||||||
|
|
||||||
|
.. versionadded:: 9.1.0
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
To enable JPEG 2000 support, you need to build and install the OpenJPEG
|
To enable JPEG 2000 support, you need to build and install the OpenJPEG
|
||||||
|
@ -743,7 +786,7 @@ parameter must be set to ``True``. The following parameters can also be set:
|
||||||
PPM
|
PPM
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L`` or
|
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or
|
||||||
``RGB`` data.
|
``RGB`` data.
|
||||||
|
|
||||||
SGI
|
SGI
|
||||||
|
|
|
@ -171,20 +171,37 @@ Rolling an image
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
def roll(image, delta):
|
def roll(im, delta):
|
||||||
"""Roll an image sideways."""
|
"""Roll an image sideways."""
|
||||||
xsize, ysize = image.size
|
xsize, ysize = im.size
|
||||||
|
|
||||||
delta = delta % xsize
|
delta = delta % xsize
|
||||||
if delta == 0:
|
if delta == 0:
|
||||||
return image
|
return im
|
||||||
|
|
||||||
part1 = image.crop((0, 0, delta, ysize))
|
part1 = im.crop((0, 0, delta, ysize))
|
||||||
part2 = image.crop((delta, 0, xsize, ysize))
|
part2 = im.crop((delta, 0, xsize, ysize))
|
||||||
image.paste(part1, (xsize - delta, 0, xsize, ysize))
|
im.paste(part1, (xsize - delta, 0, xsize, ysize))
|
||||||
image.paste(part2, (0, 0, xsize - delta, ysize))
|
im.paste(part2, (0, 0, xsize - delta, ysize))
|
||||||
|
|
||||||
return image
|
return im
|
||||||
|
|
||||||
|
Or if you would like to merge two images into a wider image:
|
||||||
|
|
||||||
|
Merging images
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
def merge(im1, im2):
|
||||||
|
w = im1.size[0] + im2.size[0]
|
||||||
|
h = max(im1.size[1], im2.size[1])
|
||||||
|
im = Image.new("RGBA", (w, h))
|
||||||
|
|
||||||
|
im.paste(im1)
|
||||||
|
im.paste(im2, (im1.size[0], 0))
|
||||||
|
|
||||||
|
return im
|
||||||
|
|
||||||
For more advanced tricks, the paste method can also take a transparency mask as
|
For more advanced tricks, the paste method can also take a transparency mask as
|
||||||
an optional argument. In this mask, the value 255 indicates that the pasted
|
an optional argument. In this mask, the value 255 indicates that the pasted
|
||||||
|
|
|
@ -123,8 +123,12 @@ The ``tile`` attribute
|
||||||
To be able to read the file as well as just identifying it, the ``tile``
|
To be able to read the file as well as just identifying it, the ``tile``
|
||||||
attribute must also be set. This attribute consists of a list of tile
|
attribute must also be set. This attribute consists of a list of tile
|
||||||
descriptors, where each descriptor specifies how data should be loaded to a
|
descriptors, where each descriptor specifies how data should be loaded to a
|
||||||
given region in the image. In most cases, only a single descriptor is used,
|
given region in the image.
|
||||||
covering the full image.
|
|
||||||
|
In most cases, only a single descriptor is used, covering the full image.
|
||||||
|
:py:class:`.PsdImagePlugin.PsdImageFile` uses multiple tiles to combine
|
||||||
|
channels within a single layer, given that the channels are stored separately,
|
||||||
|
one after the other.
|
||||||
|
|
||||||
The tile descriptor is a 4-tuple with the following contents::
|
The tile descriptor is a 4-tuple with the following contents::
|
||||||
|
|
||||||
|
@ -324,42 +328,42 @@ The fields are used as follows:
|
||||||
Whether the first line in the image is the top line on the screen (1), or
|
Whether the first line in the image is the top line on the screen (1), or
|
||||||
the bottom line (-1). If omitted, the orientation defaults to 1.
|
the bottom line (-1). If omitted, the orientation defaults to 1.
|
||||||
|
|
||||||
.. _file-decoders:
|
.. _file-codecs:
|
||||||
|
|
||||||
Writing Your Own File Decoder in C
|
Writing Your Own File Codec in C
|
||||||
==================================
|
================================
|
||||||
|
|
||||||
There are 3 stages in a file decoder's lifetime:
|
There are 3 stages in a file codec's lifetime:
|
||||||
|
|
||||||
1. Setup: Pillow looks for a function in the decoder registry, falling
|
1. Setup: Pillow looks for a function in the decoder or encoder registry,
|
||||||
back to a function named ``[decodername]_decoder`` on the internal
|
falling back to a function named ``[codecname]_decoder`` or
|
||||||
core image object. That function is called with the ``args`` tuple
|
``[codecname]_encoder`` on the internal core image object. That function is
|
||||||
from the ``tile`` setup in the ``_open`` method.
|
called with the ``args`` tuple from the ``tile``.
|
||||||
|
|
||||||
2. Decoding: The decoder's decode function is repeatedly called with
|
2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly
|
||||||
chunks of image data.
|
called with chunks of image data.
|
||||||
|
|
||||||
3. Cleanup: If the decoder has registered a cleanup function, it will
|
3. Cleanup: If the codec has registered a cleanup function, it will
|
||||||
be called at the end of the decoding process, even if there was an
|
be called at the end of the transformation process, even if there was an
|
||||||
exception raised.
|
exception raised.
|
||||||
|
|
||||||
|
|
||||||
Setup
|
Setup
|
||||||
-----
|
-----
|
||||||
|
|
||||||
The current conventions are that the decoder setup function is named
|
The current conventions are that the codec setup function is named
|
||||||
``PyImaging_[Decodername]DecoderNew`` and defined in ``decode.c``. The
|
``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew``
|
||||||
python binding for it is named ``[decodername]_decoder`` and is setup
|
and defined in ``decode.c`` or ``encode.c``. The Python binding for it is
|
||||||
from within the ``_imaging.c`` file in the codecs section of the
|
named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from
|
||||||
function array.
|
within the ``_imaging.c`` file in the codecs section of the function array.
|
||||||
|
|
||||||
The setup function needs to call ``PyImaging_DecoderNew`` and at the
|
The setup function needs to call ``PyImaging_DecoderNew`` or
|
||||||
very least, set the ``decode`` function pointer. The fields of
|
``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or
|
||||||
interest in this object are:
|
``encode`` function pointer. The fields of interest in this object are:
|
||||||
|
|
||||||
**decode**
|
**decode**/**encode**
|
||||||
Function pointer to the decode function, which has access to
|
Function pointer to the decode or encode function, which has access to
|
||||||
``im``, ``state``, and the buffer of data to be added to the image.
|
``im``, ``state``, and the buffer of data to be transformed.
|
||||||
|
|
||||||
**cleanup**
|
**cleanup**
|
||||||
Function pointer to the cleanup function, has access to ``state``.
|
Function pointer to the cleanup function, has access to ``state``.
|
||||||
|
@ -369,36 +373,34 @@ interest in this object are:
|
||||||
|
|
||||||
**state**
|
**state**
|
||||||
An ImagingCodecStateInstance, will be set by Pillow. The ``context``
|
An ImagingCodecStateInstance, will be set by Pillow. The ``context``
|
||||||
member is an opaque struct that can be used by the decoder to store
|
member is an opaque struct that can be used by the codec to store
|
||||||
any format specific state or options.
|
any format specific state or options.
|
||||||
|
|
||||||
**pulls_fd**
|
**pulls_fd**/**pushes_fd**
|
||||||
**EXPERIMENTAL** -- **WARNING**, interface may change. If set to 1,
|
If the decoder has ``pulls_fd`` or the encoder has ``pushes_fd`` set to 1,
|
||||||
``state->fd`` will be a pointer to the Python file like object. The
|
``state->fd`` will be a pointer to the Python file like object. The codec may
|
||||||
decoder may use the functions in ``codec_fd.c`` to read directly
|
use the functions in ``codec_fd.c`` to read or write directly with the file
|
||||||
from the file like object rather than have the data pushed through a
|
like object rather than have the data pushed through a buffer.
|
||||||
buffer. Note that this implementation may be refactored until this
|
|
||||||
warning is removed.
|
|
||||||
|
|
||||||
.. versionadded:: 3.3.0
|
.. versionadded:: 3.3.0
|
||||||
|
|
||||||
|
|
||||||
Decoding
|
Transforming
|
||||||
--------
|
------------
|
||||||
|
|
||||||
The decode function is called with the target (core) image, the
|
The decode or encode function is called with the target (core) image, the codec
|
||||||
decoder state structure, and a buffer of data to be decoded.
|
state structure, and a buffer of data to be transformed.
|
||||||
|
|
||||||
**Experimental** -- If ``pulls_fd`` is set, then the decode function
|
It is the codec's responsibility to pull as much data as possible out of the
|
||||||
is called once, with an empty buffer. It is the decoder's
|
buffer and return the number of bytes consumed. The next call to the codec will
|
||||||
responsibility to decode the entire tile in that one call. The rest of
|
include the previous unconsumed tail. The codec function will be called
|
||||||
this section only applies if ``pulls_fd`` is not set.
|
multiple times as the data processed.
|
||||||
|
|
||||||
It is the decoder's responsibility to pull as much data as possible
|
Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or
|
||||||
out of the buffer and return the number of bytes consumed. The next
|
encode function is called once, with an empty buffer. It is the codec's
|
||||||
call to the decoder will include the previous unconsumed tail. The
|
responsibility to transform the entire tile in that one call. Using this will
|
||||||
decoder function will be called multiple times as the data is read
|
provide a codec with more freedom, but that freedom may mean increased memory
|
||||||
from the file like object.
|
usage if the entire tile is held in memory at once by the codec.
|
||||||
|
|
||||||
If an error occurs, set ``state->errcode`` and return -1.
|
If an error occurs, set ``state->errcode`` and return -1.
|
||||||
|
|
||||||
|
@ -407,10 +409,9 @@ Return -1 on success, without setting the errcode.
|
||||||
Cleanup
|
Cleanup
|
||||||
-------
|
-------
|
||||||
|
|
||||||
The cleanup function is called after the decoder returns a negative
|
The cleanup function is called after the codec returns a negative
|
||||||
value, or if there is a read error from the file. This function should
|
value, or if there is an error. This function should free any allocated
|
||||||
free any allocated memory and release any resources from external
|
memory and release any resources from external libraries.
|
||||||
libraries.
|
|
||||||
|
|
||||||
.. _file-codecs-py:
|
.. _file-codecs-py:
|
||||||
|
|
||||||
|
@ -425,11 +426,32 @@ They should be registered using :py:meth:`PIL.Image.register_decoder` and
|
||||||
the file codecs, there are three stages in the lifetime of a
|
the file codecs, there are three stages in the lifetime of a
|
||||||
Python-based file codec:
|
Python-based file codec:
|
||||||
|
|
||||||
1. Setup: Pillow looks for the decoder in the registry, then
|
1. Setup: Pillow looks for the codec in the decoder or encoder registry, then
|
||||||
instantiates the class.
|
instantiates the class.
|
||||||
|
|
||||||
2. Transforming: The instance's ``decode`` method is repeatedly called with
|
2. Transforming: The instance's ``decode`` method is repeatedly called with
|
||||||
a buffer of data to be interpreted, or the ``encode`` method is repeatedly
|
a buffer of data to be interpreted, or the ``encode`` method is repeatedly
|
||||||
called with the size of data to be output.
|
called with the size of data to be output.
|
||||||
|
|
||||||
3. Cleanup: The instance's ``cleanup`` method is called.
|
Alternatively, if the decoder's ``_pulls_fd`` property (or the encoder's
|
||||||
|
``_pushes_fd`` property) is set to ``True``, then ``decode`` and ``encode``
|
||||||
|
will only be called once. In the decoder, ``self.fd`` can be used to access
|
||||||
|
the file-like object. Using this will provide a codec with more freedom, but
|
||||||
|
that freedom may mean increased memory usage if entire file is held in
|
||||||
|
memory at once by the codec.
|
||||||
|
|
||||||
|
In ``decode``, once the data has been interpreted, ``set_as_raw`` can be
|
||||||
|
used to populate the image.
|
||||||
|
|
||||||
|
3. Cleanup: The instance's ``cleanup`` method is called once the transformation
|
||||||
|
is complete. This can be used to clean up any resources used by the codec.
|
||||||
|
|
||||||
|
If you set ``_pulls_fd`` or ``_pushes_fd`` to ``True`` however, then you
|
||||||
|
probably chose to perform any cleanup tasks at the end of ``decode`` or
|
||||||
|
``encode``.
|
||||||
|
|
||||||
|
For an example :py:class:`PIL.ImageFile.PyDecoder`, see `DdsImagePlugin
|
||||||
|
<https://github.com/python-pillow/Pillow/blob/main/docs/example/DdsImagePlugin.py>`_.
|
||||||
|
For a plugin that uses both :py:class:`PIL.ImageFile.PyDecoder` and
|
||||||
|
:py:class:`PIL.ImageFile.PyEncoder`, see `BlpImagePlugin
|
||||||
|
<https://github.com/python-pillow/Pillow/blob/main/src/PIL/BlpImagePlugin.py>`_
|
||||||
|
|
|
@ -461,8 +461,6 @@ These platforms are built and tested for every change.
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Debian 11 Bullseye | 3.9 | x86 |
|
| Debian 11 Bullseye | 3.9 | x86 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Fedora 34 | 3.9 | x86-64 |
|
|
||||||
+----------------------------------+----------------------------+---------------------+
|
|
||||||
| Fedora 35 | 3.10 | x86-64 |
|
| Fedora 35 | 3.10 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Gentoo | 3.9 | x86-64 |
|
| Gentoo | 3.9 | x86-64 |
|
||||||
|
|
|
@ -14,6 +14,16 @@ for a region of an image.
|
||||||
statistics. You can also pass in a previously calculated histogram.
|
statistics. You can also pass in a previously calculated histogram.
|
||||||
|
|
||||||
:param image: A PIL image, or a precalculated histogram.
|
:param image: A PIL image, or a precalculated histogram.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
For a PIL image, calculations rely on the
|
||||||
|
:py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are
|
||||||
|
grouped into 256 bins, even if the image has more than 8 bits per
|
||||||
|
channel. So ``I`` and ``F`` mode images have a maximum ``mean``,
|
||||||
|
``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum
|
||||||
|
of more than 255.
|
||||||
|
|
||||||
:param mask: An optional mask.
|
:param mask: An optional mask.
|
||||||
|
|
||||||
.. py:attribute:: extrema
|
.. py:attribute:: extrema
|
||||||
|
|
|
@ -6,7 +6,13 @@
|
||||||
The PixelAccess class provides read and write access to
|
The PixelAccess class provides read and write access to
|
||||||
:py:class:`PIL.Image` data at a pixel level.
|
:py:class:`PIL.Image` data at a pixel level.
|
||||||
|
|
||||||
.. note:: Accessing individual pixels is fairly slow. If you are looping over all of the pixels in an image, there is likely a faster way using other parts of the Pillow API.
|
.. note:: Accessing individual pixels is fairly slow. If you are
|
||||||
|
looping over all of the pixels in an image, there is likely
|
||||||
|
a faster way using other parts of the Pillow API.
|
||||||
|
|
||||||
|
:mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps`
|
||||||
|
have methods for many standard operations. If you wish to perform
|
||||||
|
a custom mapping, check out :py:meth:`~PIL.Image.Image.point`.
|
||||||
|
|
||||||
Example
|
Example
|
||||||
-------
|
-------
|
||||||
|
@ -39,7 +45,7 @@ Access using negative indexes is also possible.
|
||||||
|
|
||||||
|
|
||||||
:py:class:`PixelAccess` Class
|
:py:class:`PixelAccess` Class
|
||||||
-----------------------------------
|
-----------------------------
|
||||||
|
|
||||||
.. class:: PixelAccess
|
.. class:: PixelAccess
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,12 @@
|
||||||
The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version.
|
The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version.
|
||||||
|
|
||||||
.. note:: Accessing individual pixels is fairly slow. If you are
|
.. note:: Accessing individual pixels is fairly slow. If you are
|
||||||
looping over all of the pixels in an image, there is likely
|
looping over all of the pixels in an image, there is likely
|
||||||
a faster way using other parts of the Pillow API.
|
a faster way using other parts of the Pillow API.
|
||||||
|
|
||||||
|
:mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps`
|
||||||
|
have methods for many standard operations. If you wish to perform
|
||||||
|
a custom mapping, check out :py:meth:`~PIL.Image.Image.point`.
|
||||||
|
|
||||||
Example
|
Example
|
||||||
-------
|
-------
|
||||||
|
|
|
@ -146,12 +146,24 @@ At present, the information within each block is merely returned as a dictionary
|
||||||
"data" entry. This will allow more useful information to be added in the future without
|
"data" entry. This will allow more useful information to be added in the future without
|
||||||
breaking backwards compatibility.
|
breaking backwards compatibility.
|
||||||
|
|
||||||
Added rawmode argument to Image.getpalette()
|
Added mct and no_jp2 options for saving JPEG 2000
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
By default, :py:meth:`~PIL.Image.Image.getpalette` returns RGB data from the palette.
|
The :py:meth:`PIL.Image.Image.save` method now supports the following options for
|
||||||
A ``rawmode`` argument has been added, to allow the mode to be chosen instead. ``None``
|
JPEG 2000:
|
||||||
can be used to return data in the current mode of the palette.
|
|
||||||
|
**mct**
|
||||||
|
If ``1`` then enable multiple component transformation when encoding,
|
||||||
|
otherwise use ``0`` for no component transformation (default). If MCT is
|
||||||
|
enabled and ``irreversible`` is ``True`` then the Irreversible Color
|
||||||
|
Transformation will be applied, otherwise encoding will use the
|
||||||
|
Reversible Color Transformation. MCT works best with a ``mode`` of
|
||||||
|
``RGB`` and is only applicable when the image data has 3 components.
|
||||||
|
|
||||||
|
**no_jp2**
|
||||||
|
If ``True`` then don't wrap the raw codestream in the JP2 file format when
|
||||||
|
saving, otherwise the extension of the filename will be used to determine
|
||||||
|
the format (default).
|
||||||
|
|
||||||
Added PyEncoder
|
Added PyEncoder
|
||||||
^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^
|
||||||
|
@ -160,9 +172,35 @@ Added PyEncoder
|
||||||
written in Python. See :ref:`Writing Your Own File Codec in Python<file-codecs-py>` for
|
written in Python. See :ref:`Writing Your Own File Codec in Python<file-codecs-py>` for
|
||||||
more information.
|
more information.
|
||||||
|
|
||||||
|
GifImagePlugin loading strategy
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This
|
||||||
|
behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as
|
||||||
|
well.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from PIL import GifImagePlugin
|
||||||
|
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
|
||||||
|
|
||||||
|
Or subsequent frames can be kept in ``P`` mode as long as there is only a single
|
||||||
|
palette.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from PIL import GifImagePlugin
|
||||||
|
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
|
||||||
|
|
||||||
Other Changes
|
Other Changes
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
musllinux wheels
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Pillow now builds binary wheels for musllinux, suitable for Linux distributions based on the musl C standard library such as Alpine
|
||||||
|
(rather than the glibc library used by manylinux wheels). See :pep:`656`.
|
||||||
|
|
||||||
ImageShow temporary files on Unix
|
ImageShow temporary files on Unix
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@ -177,6 +215,11 @@ Image._repr_pretty_
|
||||||
identity of the object. This allows Jupyter to describe an image and have that
|
identity of the object. This allows Jupyter to describe an image and have that
|
||||||
description stay the same on subsequent executions of the same code.
|
description stay the same on subsequent executions of the same code.
|
||||||
|
|
||||||
|
Added BigTIFF reading
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Support has been added for reading BigTIFF images.
|
||||||
|
|
||||||
Added BLP saving
|
Added BLP saving
|
||||||
^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
|
|
||||||
.wy-menu-vertical li.toctree-l2.current a,
|
|
||||||
.wy-menu-vertical li.toctree-l3.current a {
|
|
||||||
background-color: #c9c9c9;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
th p {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rst-content tr .line-block {
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
jQuery(document).ready(function ($) {
|
|
||||||
setTimeout(function () {
|
|
||||||
var sectionID = 'base';
|
|
||||||
var search = function ($section, $sidebarItem) {
|
|
||||||
$section.children('.section, .function, .method').each(function () {
|
|
||||||
if ($(this).hasClass('section')) {
|
|
||||||
sectionID = $(this).attr('id');
|
|
||||||
search($(this), $sidebarItem.parent().find('[href="#'+sectionID+'"]'));
|
|
||||||
} else {
|
|
||||||
var $dt = $(this).children('dt');
|
|
||||||
var id = $dt.attr('id');
|
|
||||||
if (id === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var $functionsUL = $sidebarItem.siblings('[data-sectionID='+sectionID+']');
|
|
||||||
if (!$functionsUL.length) {
|
|
||||||
$functionsUL = $('<ul />').attr('data-sectionID', sectionID);
|
|
||||||
$functionsUL.insertAfter($sidebarItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
var $li = $('<li />');
|
|
||||||
var $a = $('<a />').css('font-size', '11.5px');
|
|
||||||
var $upperA = $sidebarItem.parent().children('a');
|
|
||||||
var $upperAParent = $upperA.parent();
|
|
||||||
if ($upperAParent.hasClass('toctree-l2')) {
|
|
||||||
$a.css('padding-left', '4em');
|
|
||||||
} else if ($upperAParent.hasClass('toctree-l3')) {
|
|
||||||
if (!$upperA.find('.toctree-expand').length) {
|
|
||||||
$upperA.prepend($('<span />').addClass('toctree-expand'));
|
|
||||||
}
|
|
||||||
$a.css('padding-left', '5em');
|
|
||||||
} else {
|
|
||||||
$a.css('background-color', '#bdbdbd');
|
|
||||||
$a.css('padding-left', '6.25em');
|
|
||||||
}
|
|
||||||
$a.attr('href', '#'+id);
|
|
||||||
$a.text('- '+$dt.find('code').text());
|
|
||||||
$a.click(function () {
|
|
||||||
setTimeout(function () {
|
|
||||||
$a.css('font-weight', 'bold');
|
|
||||||
}, 0);
|
|
||||||
});
|
|
||||||
$li.append($a);
|
|
||||||
$functionsUL.append($li);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
search($('[itemprop=articleBody] > .section'), $('.wy-nav-side a[href="#"]'));
|
|
||||||
}, 0);
|
|
||||||
$(window).on('hashchange', function () {
|
|
||||||
$('ul[data-sectionID]').each(function () {
|
|
||||||
$(this).find('a').each(function () {
|
|
||||||
$(this).css('font-weight', 'normal');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
BIN
docs/resources/pillow-logo-dark-text.png
Normal file
After Width: | Height: | Size: 16 KiB |
|
@ -37,12 +37,12 @@ python_requires = >=3.7
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
docs =
|
docs =
|
||||||
|
furo
|
||||||
olefile
|
olefile
|
||||||
sphinx>=2.4
|
sphinx>=2.4
|
||||||
sphinx-copybutton
|
sphinx-copybutton
|
||||||
sphinx-issues>=3.0.1
|
sphinx-issues>=3.0.1
|
||||||
sphinx-removed-in
|
sphinx-removed-in
|
||||||
sphinx-rtd-theme>=1.0
|
|
||||||
sphinxext-opengraph
|
sphinxext-opengraph
|
||||||
tests =
|
tests =
|
||||||
check-manifest
|
check-manifest
|
||||||
|
|
2
setup.py
|
@ -167,7 +167,7 @@ def _find_library_dirs_ldconfig():
|
||||||
# Assuming GLIBC's ldconfig (with option -p)
|
# Assuming GLIBC's ldconfig (with option -p)
|
||||||
# Alpine Linux uses musl that can't print cache
|
# Alpine Linux uses musl that can't print cache
|
||||||
args = ["/sbin/ldconfig", "-p"]
|
args = ["/sbin/ldconfig", "-p"]
|
||||||
expr = fr".*\({abi_type}.*\) => (.*)"
|
expr = rf".*\({abi_type}.*\) => (.*)"
|
||||||
env = dict(os.environ)
|
env = dict(os.environ)
|
||||||
env["LC_ALL"] = "C"
|
env["LC_ALL"] = "C"
|
||||||
env["LANG"] = "C"
|
env["LANG"] = "C"
|
||||||
|
|
|
@ -306,7 +306,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
||||||
self._load()
|
self._load()
|
||||||
except struct.error as e:
|
except struct.error as e:
|
||||||
raise OSError("Truncated BLP file") from e
|
raise OSError("Truncated BLP file") from e
|
||||||
return 0, 0
|
return -1, 0
|
||||||
|
|
||||||
def _read_blp_header(self):
|
def _read_blp_header(self):
|
||||||
self.fd.seek(4)
|
self.fd.seek(4)
|
||||||
|
|
|
@ -24,6 +24,8 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from . import Image, ImageFile, ImagePalette
|
from . import Image, ImageFile, ImagePalette
|
||||||
from ._binary import i16le as i16
|
from ._binary import i16le as i16
|
||||||
from ._binary import i32le as i32
|
from ._binary import i32le as i32
|
||||||
|
@ -102,7 +104,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
||||||
file_info["height"] = (
|
file_info["height"] = (
|
||||||
i32(header_data, 4)
|
i32(header_data, 4)
|
||||||
if not file_info["y_flip"]
|
if not file_info["y_flip"]
|
||||||
else 2 ** 32 - i32(header_data, 4)
|
else 2**32 - i32(header_data, 4)
|
||||||
)
|
)
|
||||||
file_info["planes"] = i16(header_data, 8)
|
file_info["planes"] = i16(header_data, 8)
|
||||||
file_info["bits"] = i16(header_data, 10)
|
file_info["bits"] = i16(header_data, 10)
|
||||||
|
@ -167,6 +169,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
||||||
raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})")
|
raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})")
|
||||||
|
|
||||||
# ---------------- Process BMP with Bitfields compression (not palette)
|
# ---------------- Process BMP with Bitfields compression (not palette)
|
||||||
|
decoder_name = "raw"
|
||||||
if file_info["compression"] == self.BITFIELDS:
|
if file_info["compression"] == self.BITFIELDS:
|
||||||
SUPPORTED = {
|
SUPPORTED = {
|
||||||
32: [
|
32: [
|
||||||
|
@ -208,6 +211,8 @@ class BmpImageFile(ImageFile.ImageFile):
|
||||||
elif file_info["compression"] == self.RAW:
|
elif file_info["compression"] == self.RAW:
|
||||||
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
|
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
|
||||||
raw_mode, self.mode = "BGRA", "RGBA"
|
raw_mode, self.mode = "BGRA", "RGBA"
|
||||||
|
elif file_info["compression"] == self.RLE8:
|
||||||
|
decoder_name = "bmp_rle"
|
||||||
else:
|
else:
|
||||||
raise OSError(f"Unsupported BMP compression ({file_info['compression']})")
|
raise OSError(f"Unsupported BMP compression ({file_info['compression']})")
|
||||||
|
|
||||||
|
@ -247,7 +252,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
||||||
self.info["compression"] = file_info["compression"]
|
self.info["compression"] = file_info["compression"]
|
||||||
self.tile = [
|
self.tile = [
|
||||||
(
|
(
|
||||||
"raw",
|
decoder_name,
|
||||||
(0, 0, file_info["width"], file_info["height"]),
|
(0, 0, file_info["width"], file_info["height"]),
|
||||||
offset or self.fp.tell(),
|
offset or self.fp.tell(),
|
||||||
(
|
(
|
||||||
|
@ -271,6 +276,57 @@ class BmpImageFile(ImageFile.ImageFile):
|
||||||
self._bitmap(offset=offset)
|
self._bitmap(offset=offset)
|
||||||
|
|
||||||
|
|
||||||
|
class BmpRleDecoder(ImageFile.PyDecoder):
|
||||||
|
_pulls_fd = True
|
||||||
|
|
||||||
|
def decode(self, buffer):
|
||||||
|
data = bytearray()
|
||||||
|
x = 0
|
||||||
|
while len(data) < self.state.xsize * self.state.ysize:
|
||||||
|
pixels = self.fd.read(1)
|
||||||
|
byte = self.fd.read(1)
|
||||||
|
if not pixels or not byte:
|
||||||
|
break
|
||||||
|
num_pixels = pixels[0]
|
||||||
|
if num_pixels:
|
||||||
|
# encoded mode
|
||||||
|
if x + num_pixels > self.state.xsize:
|
||||||
|
# Too much data for row
|
||||||
|
num_pixels = max(0, self.state.xsize - x)
|
||||||
|
data += byte * num_pixels
|
||||||
|
x += num_pixels
|
||||||
|
else:
|
||||||
|
if byte[0] == 0:
|
||||||
|
# end of line
|
||||||
|
while len(data) % self.state.xsize != 0:
|
||||||
|
data += b"\x00"
|
||||||
|
x = 0
|
||||||
|
elif byte[0] == 1:
|
||||||
|
# end of bitmap
|
||||||
|
break
|
||||||
|
elif byte[0] == 2:
|
||||||
|
# delta
|
||||||
|
bytes_read = self.fd.read(2)
|
||||||
|
if len(bytes_read) < 2:
|
||||||
|
break
|
||||||
|
right, up = self.fd.read(2)
|
||||||
|
data += b"\x00" * (right + up * self.state.xsize)
|
||||||
|
x = len(data) % self.state.xsize
|
||||||
|
else:
|
||||||
|
# absolute mode
|
||||||
|
bytes_read = self.fd.read(byte[0])
|
||||||
|
data += bytes_read
|
||||||
|
if len(bytes_read) < byte[0]:
|
||||||
|
break
|
||||||
|
x += byte[0]
|
||||||
|
|
||||||
|
# align to 16-bit word boundary
|
||||||
|
if self.fd.tell() % 2 != 0:
|
||||||
|
self.fd.seek(1, os.SEEK_CUR)
|
||||||
|
self.set_as_raw(bytes(data), ("P", 0, self.args[-1]))
|
||||||
|
return -1, 0
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Image plugin for the DIB format (BMP alias)
|
# Image plugin for the DIB format (BMP alias)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
@ -322,7 +378,7 @@ def _save(im, fp, filename, bitmap_header=True):
|
||||||
if bitmap_header:
|
if bitmap_header:
|
||||||
offset = 14 + header + colors * 4
|
offset = 14 + header + colors * 4
|
||||||
file_size = offset + image
|
file_size = offset + image
|
||||||
if file_size > 2 ** 32 - 1:
|
if file_size > 2**32 - 1:
|
||||||
raise ValueError("File size is too large for the BMP format")
|
raise ValueError("File size is too large for the BMP format")
|
||||||
fp.write(
|
fp.write(
|
||||||
b"BM" # file type (magic)
|
b"BM" # file type (magic)
|
||||||
|
@ -372,6 +428,8 @@ Image.register_extension(BmpImageFile.format, ".bmp")
|
||||||
|
|
||||||
Image.register_mime(BmpImageFile.format, "image/bmp")
|
Image.register_mime(BmpImageFile.format, "image/bmp")
|
||||||
|
|
||||||
|
Image.register_decoder("bmp_rle", BmpRleDecoder)
|
||||||
|
|
||||||
Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
|
Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
|
||||||
Image.register_save(DibImageFile.format, _dib_save)
|
Image.register_save(DibImageFile.format, _dib_save)
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,9 @@ class DdsImageFile(ImageFile.ImageFile):
|
||||||
format_description = "DirectDraw Surface"
|
format_description = "DirectDraw Surface"
|
||||||
|
|
||||||
def _open(self):
|
def _open(self):
|
||||||
magic, header_size = struct.unpack("<II", self.fp.read(8))
|
if not _accept(self.fp.read(4)):
|
||||||
|
raise SyntaxError("not a DDS file")
|
||||||
|
(header_size,) = struct.unpack("<I", self.fp.read(4))
|
||||||
if header_size != 124:
|
if header_size != 124:
|
||||||
raise OSError(f"Unsupported header size {repr(header_size)}")
|
raise OSError(f"Unsupported header size {repr(header_size)}")
|
||||||
header_bytes = self.fp.read(header_size - 4)
|
header_bytes = self.fp.read(header_size - 4)
|
||||||
|
|
|
@ -26,7 +26,11 @@ from ._binary import o8
|
||||||
|
|
||||||
|
|
||||||
def _accept(prefix):
|
def _accept(prefix):
|
||||||
return len(prefix) >= 6 and i16(prefix, 4) in [0xAF11, 0xAF12]
|
return (
|
||||||
|
len(prefix) >= 6
|
||||||
|
and i16(prefix, 4) in [0xAF11, 0xAF12]
|
||||||
|
and i16(prefix, 14) in [0, 3] # flags
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -44,11 +48,7 @@ class FliImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
# HEAD
|
# HEAD
|
||||||
s = self.fp.read(128)
|
s = self.fp.read(128)
|
||||||
if not (
|
if not (_accept(s) and s[20:22] == b"\x00\x00"):
|
||||||
_accept(s)
|
|
||||||
and i16(s, 14) in [0, 3] # flags
|
|
||||||
and s[20:22] == b"\x00\x00" # reserved
|
|
||||||
):
|
|
||||||
raise SyntaxError("not an FLI/FLC file")
|
raise SyntaxError("not an FLI/FLC file")
|
||||||
|
|
||||||
# frames
|
# frames
|
||||||
|
|
|
@ -94,7 +94,8 @@ class FtexImageFile(ImageFile.ImageFile):
|
||||||
format_description = "Texture File Format (IW2:EOC)"
|
format_description = "Texture File Format (IW2:EOC)"
|
||||||
|
|
||||||
def _open(self):
|
def _open(self):
|
||||||
struct.unpack("<I", self.fp.read(4)) # magic
|
if not _accept(self.fp.read(4)):
|
||||||
|
raise SyntaxError("not an FTEX file")
|
||||||
struct.unpack("<i", self.fp.read(4)) # version
|
struct.unpack("<i", self.fp.read(4)) # version
|
||||||
self._size = struct.unpack("<2i", self.fp.read(8))
|
self._size = struct.unpack("<2i", self.fp.read(8))
|
||||||
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
|
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
|
||||||
|
|
|
@ -43,9 +43,9 @@ class GbrImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
def _open(self):
|
def _open(self):
|
||||||
header_size = i32(self.fp.read(4))
|
header_size = i32(self.fp.read(4))
|
||||||
version = i32(self.fp.read(4))
|
|
||||||
if header_size < 20:
|
if header_size < 20:
|
||||||
raise SyntaxError("not a GIMP brush")
|
raise SyntaxError("not a GIMP brush")
|
||||||
|
version = i32(self.fp.read(4))
|
||||||
if version not in (1, 2):
|
if version not in (1, 2):
|
||||||
raise SyntaxError(f"Unsupported GIMP brush version: {version}")
|
raise SyntaxError(f"Unsupported GIMP brush version: {version}")
|
||||||
|
|
||||||
|
|
|
@ -28,12 +28,25 @@ import itertools
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
|
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
|
||||||
from ._binary import i16le as i16
|
from ._binary import i16le as i16
|
||||||
from ._binary import o8
|
from ._binary import o8
|
||||||
from ._binary import o16le as o16
|
from ._binary import o16le as o16
|
||||||
|
|
||||||
|
|
||||||
|
class LoadingStrategy(IntEnum):
|
||||||
|
""".. versionadded:: 9.1.0"""
|
||||||
|
|
||||||
|
RGB_AFTER_FIRST = 0
|
||||||
|
RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1
|
||||||
|
RGB_ALWAYS = 2
|
||||||
|
|
||||||
|
|
||||||
|
#: .. versionadded:: 9.1.0
|
||||||
|
LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# Identify/read GIF files
|
# Identify/read GIF files
|
||||||
|
|
||||||
|
@ -61,6 +74,12 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
return self.fp.read(s[0])
|
return self.fp.read(s[0])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _is_palette_needed(self, p):
|
||||||
|
for i in range(0, len(p), 3):
|
||||||
|
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def _open(self):
|
def _open(self):
|
||||||
|
|
||||||
# Screen
|
# Screen
|
||||||
|
@ -79,11 +98,9 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
self.info["background"] = s[11]
|
self.info["background"] = s[11]
|
||||||
# check if palette contains colour indices
|
# check if palette contains colour indices
|
||||||
p = self.fp.read(3 << bits)
|
p = self.fp.read(3 << bits)
|
||||||
for i in range(0, len(p), 3):
|
if self._is_palette_needed(p):
|
||||||
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
|
p = ImagePalette.raw("RGB", p)
|
||||||
p = ImagePalette.raw("RGB", p)
|
self.global_palette = self.palette = p
|
||||||
self.global_palette = self.palette = p
|
|
||||||
break
|
|
||||||
|
|
||||||
self.__fp = self.fp # FIXME: hack
|
self.__fp = self.fp # FIXME: hack
|
||||||
self.__rewind = self.fp.tell()
|
self.__rewind = self.fp.tell()
|
||||||
|
@ -97,7 +114,7 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
current = self.tell()
|
current = self.tell()
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
self.seek(self.tell() + 1)
|
self._seek(self.tell() + 1, False)
|
||||||
except EOFError:
|
except EOFError:
|
||||||
self._n_frames = self.tell() + 1
|
self._n_frames = self.tell() + 1
|
||||||
self.seek(current)
|
self.seek(current)
|
||||||
|
@ -110,14 +127,16 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
self._is_animated = self._n_frames != 1
|
self._is_animated = self._n_frames != 1
|
||||||
else:
|
else:
|
||||||
current = self.tell()
|
current = self.tell()
|
||||||
|
if current:
|
||||||
try:
|
|
||||||
self.seek(1)
|
|
||||||
self._is_animated = True
|
self._is_animated = True
|
||||||
except EOFError:
|
else:
|
||||||
self._is_animated = False
|
try:
|
||||||
|
self._seek(1, False)
|
||||||
|
self._is_animated = True
|
||||||
|
except EOFError:
|
||||||
|
self._is_animated = False
|
||||||
|
|
||||||
self.seek(current)
|
self.seek(current)
|
||||||
return self._is_animated
|
return self._is_animated
|
||||||
|
|
||||||
def seek(self, frame):
|
def seek(self, frame):
|
||||||
|
@ -135,26 +154,22 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
self.seek(last_frame)
|
self.seek(last_frame)
|
||||||
raise EOFError("no more images in GIF file") from e
|
raise EOFError("no more images in GIF file") from e
|
||||||
|
|
||||||
def _seek(self, frame):
|
def _seek(self, frame, update_image=True):
|
||||||
|
|
||||||
if frame == 0:
|
if frame == 0:
|
||||||
# rewind
|
# rewind
|
||||||
self.__offset = 0
|
self.__offset = 0
|
||||||
self.dispose = None
|
self.dispose = None
|
||||||
self.dispose_extent = [0, 0, 0, 0] # x0, y0, x1, y1
|
|
||||||
self.__frame = -1
|
self.__frame = -1
|
||||||
self.__fp.seek(self.__rewind)
|
self.__fp.seek(self.__rewind)
|
||||||
self.disposal_method = 0
|
self.disposal_method = 0
|
||||||
else:
|
else:
|
||||||
# ensure that the previous frame was loaded
|
# ensure that the previous frame was loaded
|
||||||
if self.tile:
|
if self.tile and update_image:
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
if frame != self.__frame + 1:
|
if frame != self.__frame + 1:
|
||||||
raise ValueError(f"cannot seek to frame {frame}")
|
raise ValueError(f"cannot seek to frame {frame}")
|
||||||
self.__frame = frame
|
|
||||||
|
|
||||||
self.tile = []
|
|
||||||
|
|
||||||
self.fp = self.__fp
|
self.fp = self.__fp
|
||||||
if self.__offset:
|
if self.__offset:
|
||||||
|
@ -164,28 +179,24 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
pass
|
pass
|
||||||
self.__offset = 0
|
self.__offset = 0
|
||||||
|
|
||||||
if self.__frame == 1:
|
s = self.fp.read(1)
|
||||||
self.pyaccess = None
|
if not s or s == b";":
|
||||||
if "transparency" in self.info:
|
raise EOFError
|
||||||
self.mode = "RGBA"
|
|
||||||
self.im.putpalettealpha(self.info["transparency"], 0)
|
|
||||||
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
|
|
||||||
|
|
||||||
del self.info["transparency"]
|
self.__frame = frame
|
||||||
else:
|
|
||||||
self.mode = "RGB"
|
self.tile = []
|
||||||
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
|
|
||||||
if self.dispose:
|
|
||||||
self.im.paste(self.dispose, self.dispose_extent)
|
|
||||||
|
|
||||||
palette = None
|
palette = None
|
||||||
|
|
||||||
info = {}
|
info = {}
|
||||||
frame_transparency = None
|
frame_transparency = None
|
||||||
interlace = None
|
interlace = None
|
||||||
|
frame_dispose_extent = None
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
s = self.fp.read(1)
|
if not s:
|
||||||
|
s = self.fp.read(1)
|
||||||
if not s or s == b";":
|
if not s or s == b";":
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -223,6 +234,7 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
else:
|
else:
|
||||||
info["comment"] = block
|
info["comment"] = block
|
||||||
block = self.data()
|
block = self.data()
|
||||||
|
s = None
|
||||||
continue
|
continue
|
||||||
elif s[0] == 255:
|
elif s[0] == 255:
|
||||||
#
|
#
|
||||||
|
@ -245,16 +257,18 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
# extent
|
# extent
|
||||||
x0, y0 = i16(s, 0), i16(s, 2)
|
x0, y0 = i16(s, 0), i16(s, 2)
|
||||||
x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
|
x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
|
||||||
if x1 > self.size[0] or y1 > self.size[1]:
|
if (x1 > self.size[0] or y1 > self.size[1]) and update_image:
|
||||||
self._size = max(x1, self.size[0]), max(y1, self.size[1])
|
self._size = max(x1, self.size[0]), max(y1, self.size[1])
|
||||||
self.dispose_extent = x0, y0, x1, y1
|
frame_dispose_extent = x0, y0, x1, y1
|
||||||
flags = s[8]
|
flags = s[8]
|
||||||
|
|
||||||
interlace = (flags & 64) != 0
|
interlace = (flags & 64) != 0
|
||||||
|
|
||||||
if flags & 128:
|
if flags & 128:
|
||||||
bits = (flags & 7) + 1
|
bits = (flags & 7) + 1
|
||||||
palette = ImagePalette.raw("RGB", self.fp.read(3 << bits))
|
p = self.fp.read(3 << bits)
|
||||||
|
if self._is_palette_needed(p):
|
||||||
|
palette = ImagePalette.raw("RGB", p)
|
||||||
|
|
||||||
# image data
|
# image data
|
||||||
bits = self.fp.read(1)[0]
|
bits = self.fp.read(1)[0]
|
||||||
|
@ -264,16 +278,56 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
# raise OSError, "illegal GIF tag `%x`" % s[0]
|
# raise OSError, "illegal GIF tag `%x`" % s[0]
|
||||||
|
s = None
|
||||||
|
|
||||||
frame_palette = palette or self.global_palette
|
if interlace is None:
|
||||||
|
# self.__fp = None
|
||||||
|
raise EOFError
|
||||||
|
if not update_image:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.dispose:
|
||||||
|
self.im.paste(self.dispose, self.dispose_extent)
|
||||||
|
|
||||||
|
self._frame_palette = palette or self.global_palette
|
||||||
|
if frame == 0:
|
||||||
|
if self._frame_palette:
|
||||||
|
self.mode = (
|
||||||
|
"RGB" if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS else "P"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.mode = "L"
|
||||||
|
|
||||||
|
if not palette and self.global_palette:
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
|
palette = copy(self.global_palette)
|
||||||
|
self.palette = palette
|
||||||
|
else:
|
||||||
|
self._frame_transparency = frame_transparency
|
||||||
|
if self.mode == "P":
|
||||||
|
if (
|
||||||
|
LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
|
||||||
|
or palette
|
||||||
|
):
|
||||||
|
self.pyaccess = None
|
||||||
|
if "transparency" in self.info:
|
||||||
|
self.im.putpalettealpha(self.info["transparency"], 0)
|
||||||
|
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
|
||||||
|
self.mode = "RGBA"
|
||||||
|
del self.info["transparency"]
|
||||||
|
else:
|
||||||
|
self.mode = "RGB"
|
||||||
|
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
|
||||||
|
|
||||||
def _rgb(color):
|
def _rgb(color):
|
||||||
if frame_palette:
|
if self._frame_palette:
|
||||||
color = tuple(frame_palette.palette[color * 3 : color * 3 + 3])
|
color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
|
||||||
else:
|
else:
|
||||||
color = (color, color, color)
|
color = (color, color, color)
|
||||||
return color
|
return color
|
||||||
|
|
||||||
|
self.dispose_extent = frame_dispose_extent
|
||||||
try:
|
try:
|
||||||
if self.disposal_method < 2:
|
if self.disposal_method < 2:
|
||||||
# do not dispose or none specified
|
# do not dispose or none specified
|
||||||
|
@ -288,17 +342,21 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
Image._decompression_bomb_check(dispose_size)
|
Image._decompression_bomb_check(dispose_size)
|
||||||
|
|
||||||
# by convention, attempt to use transparency first
|
# by convention, attempt to use transparency first
|
||||||
|
dispose_mode = "P"
|
||||||
color = self.info.get("transparency", frame_transparency)
|
color = self.info.get("transparency", frame_transparency)
|
||||||
if color is not None:
|
if color is not None:
|
||||||
dispose_mode = "RGBA"
|
if self.mode in ("RGB", "RGBA"):
|
||||||
color = _rgb(color) + (0,)
|
dispose_mode = "RGBA"
|
||||||
|
color = _rgb(color) + (0,)
|
||||||
else:
|
else:
|
||||||
dispose_mode = "RGB"
|
color = self.info.get("background", 0)
|
||||||
color = _rgb(self.info.get("background", 0))
|
if self.mode in ("RGB", "RGBA"):
|
||||||
|
dispose_mode = "RGB"
|
||||||
|
color = _rgb(color)
|
||||||
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
|
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
|
||||||
else:
|
else:
|
||||||
# replace with previous contents
|
# replace with previous contents
|
||||||
if self.im:
|
if self.im is not None:
|
||||||
# only dispose the extent in this frame
|
# only dispose the extent in this frame
|
||||||
self.dispose = self._crop(self.im, self.dispose_extent)
|
self.dispose = self._crop(self.im, self.dispose_extent)
|
||||||
elif frame_transparency is not None:
|
elif frame_transparency is not None:
|
||||||
|
@ -306,26 +364,30 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
dispose_size = (x1 - x0, y1 - y0)
|
dispose_size = (x1 - x0, y1 - y0)
|
||||||
|
|
||||||
Image._decompression_bomb_check(dispose_size)
|
Image._decompression_bomb_check(dispose_size)
|
||||||
self.dispose = Image.core.fill(
|
dispose_mode = "P"
|
||||||
"RGBA", dispose_size, _rgb(frame_transparency) + (0,)
|
color = frame_transparency
|
||||||
)
|
if self.mode in ("RGB", "RGBA"):
|
||||||
|
dispose_mode = "RGBA"
|
||||||
|
color = _rgb(frame_transparency) + (0,)
|
||||||
|
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if interlace is not None:
|
if interlace is not None:
|
||||||
if frame == 0 and frame_transparency is not None:
|
transparency = -1
|
||||||
self.info["transparency"] = frame_transparency
|
if frame_transparency is not None:
|
||||||
|
if frame == 0:
|
||||||
|
self.info["transparency"] = frame_transparency
|
||||||
|
elif self.mode not in ("RGB", "RGBA"):
|
||||||
|
transparency = frame_transparency
|
||||||
self.tile = [
|
self.tile = [
|
||||||
(
|
(
|
||||||
"gif",
|
"gif",
|
||||||
(x0, y0, x1, y1),
|
(x0, y0, x1, y1),
|
||||||
self.__offset,
|
self.__offset,
|
||||||
(bits, interlace),
|
(bits, interlace, transparency),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
else:
|
|
||||||
# self.__fp = None
|
|
||||||
raise EOFError
|
|
||||||
|
|
||||||
for k in ["duration", "comment", "extension", "loop"]:
|
for k in ["duration", "comment", "extension", "loop"]:
|
||||||
if k in info:
|
if k in info:
|
||||||
|
@ -333,45 +395,42 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
elif k in self.info:
|
elif k in self.info:
|
||||||
del self.info[k]
|
del self.info[k]
|
||||||
|
|
||||||
if frame == 0:
|
|
||||||
self.mode = "P" if frame_palette else "L"
|
|
||||||
|
|
||||||
if self.mode == "P" and not palette:
|
|
||||||
from copy import copy
|
|
||||||
|
|
||||||
palette = copy(self.global_palette)
|
|
||||||
self.palette = palette
|
|
||||||
else:
|
|
||||||
self._frame_palette = frame_palette
|
|
||||||
self._frame_transparency = frame_transparency
|
|
||||||
|
|
||||||
def load_prepare(self):
|
def load_prepare(self):
|
||||||
|
temp_mode = "P" if self._frame_palette else "L"
|
||||||
|
self._prev_im = None
|
||||||
if self.__frame == 0:
|
if self.__frame == 0:
|
||||||
if "transparency" in self.info:
|
if "transparency" in self.info:
|
||||||
self.im = Image.core.fill(
|
self.im = Image.core.fill(
|
||||||
self.mode, self.size, self.info["transparency"]
|
temp_mode, self.size, self.info["transparency"]
|
||||||
)
|
)
|
||||||
else:
|
elif self.mode in ("RGB", "RGBA"):
|
||||||
self._prev_im = self.im
|
self._prev_im = self.im
|
||||||
if self._frame_palette:
|
if self._frame_palette:
|
||||||
self.mode = "P"
|
|
||||||
self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
|
self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
|
||||||
self.im.putpalette(*self._frame_palette.getdata())
|
self.im.putpalette(*self._frame_palette.getdata())
|
||||||
self._frame_palette = None
|
|
||||||
else:
|
else:
|
||||||
self.mode = "L"
|
|
||||||
self.im = None
|
self.im = None
|
||||||
|
self.mode = temp_mode
|
||||||
|
self._frame_palette = None
|
||||||
|
|
||||||
super().load_prepare()
|
super().load_prepare()
|
||||||
|
|
||||||
def load_end(self):
|
def load_end(self):
|
||||||
if self.__frame == 0:
|
if self.__frame == 0:
|
||||||
|
if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
|
||||||
|
self.mode = "RGB"
|
||||||
|
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
|
||||||
return
|
return
|
||||||
if self._frame_transparency is not None:
|
if self.mode == "P" and self._prev_im:
|
||||||
self.im.putpalettealpha(self._frame_transparency, 0)
|
if self._frame_transparency is not None:
|
||||||
frame_im = self.im.convert("RGBA")
|
self.im.putpalettealpha(self._frame_transparency, 0)
|
||||||
|
frame_im = self.im.convert("RGBA")
|
||||||
|
else:
|
||||||
|
frame_im = self.im.convert("RGB")
|
||||||
else:
|
else:
|
||||||
frame_im = self.im.convert("RGB")
|
if not self._prev_im:
|
||||||
|
return
|
||||||
|
frame_im = self.im
|
||||||
frame_im = self._crop(frame_im, self.dispose_extent)
|
frame_im = self._crop(frame_im, self.dispose_extent)
|
||||||
|
|
||||||
self.im = self._prev_im
|
self.im = self._prev_im
|
||||||
|
@ -401,7 +460,7 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
RAWMODE = {"1": "L", "L": "L", "P": "P"}
|
RAWMODE = {"1": "L", "L": "L", "P": "P"}
|
||||||
|
|
||||||
|
|
||||||
def _normalize_mode(im, initial_call=False):
|
def _normalize_mode(im):
|
||||||
"""
|
"""
|
||||||
Takes an image (or frame), returns an image in a mode that is appropriate
|
Takes an image (or frame), returns an image in a mode that is appropriate
|
||||||
for saving in a Gif.
|
for saving in a Gif.
|
||||||
|
@ -409,31 +468,20 @@ def _normalize_mode(im, initial_call=False):
|
||||||
It may return the original image, or it may return an image converted to
|
It may return the original image, or it may return an image converted to
|
||||||
palette or 'L' mode.
|
palette or 'L' mode.
|
||||||
|
|
||||||
UNDONE: What is the point of mucking with the initial call palette, for
|
|
||||||
an image that shouldn't have a palette, or it would be a mode 'P' and
|
|
||||||
get returned in the RAWMODE clause.
|
|
||||||
|
|
||||||
:param im: Image object
|
:param im: Image object
|
||||||
:param initial_call: Default false, set to true for a single frame.
|
|
||||||
:returns: Image object
|
:returns: Image object
|
||||||
"""
|
"""
|
||||||
if im.mode in RAWMODE:
|
if im.mode in RAWMODE:
|
||||||
im.load()
|
im.load()
|
||||||
return im
|
return im
|
||||||
if Image.getmodebase(im.mode) == "RGB":
|
if Image.getmodebase(im.mode) == "RGB":
|
||||||
if initial_call:
|
im = im.convert("P", palette=Image.Palette.ADAPTIVE)
|
||||||
palette_size = 256
|
if im.palette.mode == "RGBA":
|
||||||
if im.palette:
|
for rgba in im.palette.colors.keys():
|
||||||
palette_size = len(im.palette.getdata()[1]) // 3
|
if rgba[3] == 0:
|
||||||
im = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=palette_size)
|
im.info["transparency"] = im.palette.colors[rgba]
|
||||||
if im.palette.mode == "RGBA":
|
break
|
||||||
for rgba in im.palette.colors.keys():
|
return im
|
||||||
if rgba[3] == 0:
|
|
||||||
im.info["transparency"] = im.palette.colors[rgba]
|
|
||||||
break
|
|
||||||
return im
|
|
||||||
else:
|
|
||||||
return im.convert("P")
|
|
||||||
return im.convert("L")
|
return im.convert("L")
|
||||||
|
|
||||||
|
|
||||||
|
@ -491,7 +539,7 @@ def _normalize_palette(im, palette, info):
|
||||||
|
|
||||||
|
|
||||||
def _write_single_frame(im, fp, palette):
|
def _write_single_frame(im, fp, palette):
|
||||||
im_out = _normalize_mode(im, True)
|
im_out = _normalize_mode(im)
|
||||||
for k, v in im_out.info.items():
|
for k, v in im_out.info.items():
|
||||||
im.encoderinfo.setdefault(k, v)
|
im.encoderinfo.setdefault(k, v)
|
||||||
im_out = _normalize_palette(im_out, palette, im.encoderinfo)
|
im_out = _normalize_palette(im_out, palette, im.encoderinfo)
|
||||||
|
@ -623,11 +671,14 @@ def get_interlace(im):
|
||||||
def _write_local_header(fp, im, offset, flags):
|
def _write_local_header(fp, im, offset, flags):
|
||||||
transparent_color_exists = False
|
transparent_color_exists = False
|
||||||
try:
|
try:
|
||||||
transparency = im.encoderinfo["transparency"]
|
if "transparency" in im.encoderinfo:
|
||||||
except KeyError:
|
transparency = im.encoderinfo["transparency"]
|
||||||
|
else:
|
||||||
|
transparency = im.info["transparency"]
|
||||||
|
transparency = int(transparency)
|
||||||
|
except (KeyError, ValueError):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
transparency = int(transparency)
|
|
||||||
# optimize the block away if transparent color is not used
|
# optimize the block away if transparent color is not used
|
||||||
transparent_color_exists = True
|
transparent_color_exists = True
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ class GimpPaletteFile:
|
||||||
break
|
break
|
||||||
|
|
||||||
# skip fields and comment lines
|
# skip fields and comment lines
|
||||||
if re.match(br"\w+:|#", s):
|
if re.match(rb"\w+:|#", s):
|
||||||
continue
|
continue
|
||||||
if len(s) > 100:
|
if len(s) > 100:
|
||||||
raise SyntaxError("bad palette file")
|
raise SyntaxError("bad palette file")
|
||||||
|
|
|
@ -167,7 +167,7 @@ class IcnsFile:
|
||||||
self.dct = dct = {}
|
self.dct = dct = {}
|
||||||
self.fobj = fobj
|
self.fobj = fobj
|
||||||
sig, filesize = nextheader(fobj)
|
sig, filesize = nextheader(fobj)
|
||||||
if sig != MAGIC:
|
if not _accept(sig):
|
||||||
raise SyntaxError("not an icns file")
|
raise SyntaxError("not an icns file")
|
||||||
i = HEADERSIZE
|
i = HEADERSIZE
|
||||||
while i < filesize:
|
while i < filesize:
|
||||||
|
@ -287,7 +287,7 @@ class IcnsImageFile(ImageFile.ImageFile):
|
||||||
)
|
)
|
||||||
|
|
||||||
px = Image.Image.load(self)
|
px = Image.Image.load(self)
|
||||||
if self.im and self.im.size == self.size:
|
if self.im is not None and self.im.size == self.size:
|
||||||
# Already loaded
|
# Already loaded
|
||||||
return px
|
return px
|
||||||
self.load_prepare()
|
self.load_prepare()
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
|
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
|
||||||
|
|
||||||
|
|
||||||
import struct
|
|
||||||
import warnings
|
import warnings
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from math import ceil, log
|
from math import ceil, log
|
||||||
|
@ -30,6 +29,8 @@ from math import ceil, log
|
||||||
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
|
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
|
||||||
from ._binary import i16le as i16
|
from ._binary import i16le as i16
|
||||||
from ._binary import i32le as i32
|
from ._binary import i32le as i32
|
||||||
|
from ._binary import o8
|
||||||
|
from ._binary import o16le as o16
|
||||||
from ._binary import o32le as o32
|
from ._binary import o32le as o32
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -40,57 +41,72 @@ _MAGIC = b"\0\0\1\0"
|
||||||
|
|
||||||
def _save(im, fp, filename):
|
def _save(im, fp, filename):
|
||||||
fp.write(_MAGIC) # (2+2)
|
fp.write(_MAGIC) # (2+2)
|
||||||
|
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
|
||||||
sizes = im.encoderinfo.get(
|
sizes = im.encoderinfo.get(
|
||||||
"sizes",
|
"sizes",
|
||||||
[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
|
[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
|
||||||
)
|
)
|
||||||
|
frames = []
|
||||||
|
provided_ims = [im] + im.encoderinfo.get("append_images", [])
|
||||||
width, height = im.size
|
width, height = im.size
|
||||||
sizes = filter(
|
for size in sorted(set(sizes)):
|
||||||
lambda x: False
|
if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256:
|
||||||
if (x[0] > width or x[1] > height or x[0] > 256 or x[1] > 256)
|
continue
|
||||||
else True,
|
|
||||||
sizes,
|
for provided_im in provided_ims:
|
||||||
)
|
if provided_im.size != size:
|
||||||
sizes = list(sizes)
|
continue
|
||||||
fp.write(struct.pack("<H", len(sizes))) # idCount(2)
|
frames.append(provided_im)
|
||||||
offset = fp.tell() + len(sizes) * 16
|
if bmp:
|
||||||
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
|
bits = BmpImagePlugin.SAVE[provided_im.mode][1]
|
||||||
provided_images = {im.size: im for im in im.encoderinfo.get("append_images", [])}
|
bits_used = [bits]
|
||||||
for size in sizes:
|
for other_im in provided_ims:
|
||||||
width, height = size
|
if other_im.size != size:
|
||||||
|
continue
|
||||||
|
bits = BmpImagePlugin.SAVE[other_im.mode][1]
|
||||||
|
if bits not in bits_used:
|
||||||
|
# Another image has been supplied for this size
|
||||||
|
# with a different bit depth
|
||||||
|
frames.append(other_im)
|
||||||
|
bits_used.append(bits)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# TODO: invent a more convenient method for proportional scalings
|
||||||
|
frame = provided_im.copy()
|
||||||
|
frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
|
||||||
|
frames.append(frame)
|
||||||
|
fp.write(o16(len(frames))) # idCount(2)
|
||||||
|
offset = fp.tell() + len(frames) * 16
|
||||||
|
for frame in frames:
|
||||||
|
width, height = frame.size
|
||||||
# 0 means 256
|
# 0 means 256
|
||||||
fp.write(struct.pack("B", width if width < 256 else 0)) # bWidth(1)
|
fp.write(o8(width if width < 256 else 0)) # bWidth(1)
|
||||||
fp.write(struct.pack("B", height if height < 256 else 0)) # bHeight(1)
|
fp.write(o8(height if height < 256 else 0)) # bHeight(1)
|
||||||
fp.write(b"\0") # bColorCount(1)
|
|
||||||
|
bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0)
|
||||||
|
fp.write(o8(colors)) # bColorCount(1)
|
||||||
fp.write(b"\0") # bReserved(1)
|
fp.write(b"\0") # bReserved(1)
|
||||||
fp.write(b"\0\0") # wPlanes(2)
|
fp.write(b"\0\0") # wPlanes(2)
|
||||||
|
fp.write(o16(bits)) # wBitCount(2)
|
||||||
tmp = provided_images.get(size)
|
|
||||||
if not tmp:
|
|
||||||
# TODO: invent a more convenient method for proportional scalings
|
|
||||||
tmp = im.copy()
|
|
||||||
tmp.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
|
|
||||||
bits = BmpImagePlugin.SAVE[tmp.mode][1] if bmp else 32
|
|
||||||
fp.write(struct.pack("<H", bits)) # wBitCount(2)
|
|
||||||
|
|
||||||
image_io = BytesIO()
|
image_io = BytesIO()
|
||||||
if bmp:
|
if bmp:
|
||||||
tmp.save(image_io, "dib")
|
frame.save(image_io, "dib")
|
||||||
|
|
||||||
if bits != 32:
|
if bits != 32:
|
||||||
and_mask = Image.new("1", tmp.size)
|
and_mask = Image.new("1", size)
|
||||||
ImageFile._save(
|
ImageFile._save(
|
||||||
and_mask, image_io, [("raw", (0, 0) + tmp.size, 0, ("1", 0, -1))]
|
and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
tmp.save(image_io, "png")
|
frame.save(image_io, "png")
|
||||||
image_io.seek(0)
|
image_io.seek(0)
|
||||||
image_bytes = image_io.read()
|
image_bytes = image_io.read()
|
||||||
if bmp:
|
if bmp:
|
||||||
image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
|
image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
|
||||||
bytes_len = len(image_bytes)
|
bytes_len = len(image_bytes)
|
||||||
fp.write(struct.pack("<I", bytes_len)) # dwBytesInRes(4)
|
fp.write(o32(bytes_len)) # dwBytesInRes(4)
|
||||||
fp.write(struct.pack("<I", offset)) # dwImageOffset(4)
|
fp.write(o32(offset)) # dwImageOffset(4)
|
||||||
current = fp.tell()
|
current = fp.tell()
|
||||||
fp.seek(offset)
|
fp.seek(offset)
|
||||||
fp.write(image_bytes)
|
fp.write(image_bytes)
|
||||||
|
@ -304,7 +320,7 @@ class IcoImageFile(ImageFile.ImageFile):
|
||||||
self._size = value
|
self._size = value
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
if self.im and self.im.size == self.size:
|
if self.im is not None and self.im.size == self.size:
|
||||||
# Already loaded
|
# Already loaded
|
||||||
return Image.Image.load(self)
|
return Image.Image.load(self)
|
||||||
im = self.ico.getimage(self.size)
|
im = self.ico.getimage(self.size)
|
||||||
|
|
|
@ -100,7 +100,7 @@ for i in range(2, 33):
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# Read IM directory
|
# Read IM directory
|
||||||
|
|
||||||
split = re.compile(br"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
|
split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
|
||||||
|
|
||||||
|
|
||||||
def number(s):
|
def number(s):
|
||||||
|
|
|
@ -49,7 +49,7 @@ except ImportError:
|
||||||
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
||||||
# Use __version__ instead.
|
# Use __version__ instead.
|
||||||
from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
|
from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
|
||||||
from ._binary import i32le
|
from ._binary import i32le, o32be, o32le
|
||||||
from ._util import deferred_error, isPath
|
from ._util import deferred_error, isPath
|
||||||
|
|
||||||
|
|
||||||
|
@ -847,7 +847,7 @@ class Image:
|
||||||
:returns: An image access object.
|
:returns: An image access object.
|
||||||
:rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess`
|
:rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess`
|
||||||
"""
|
"""
|
||||||
if self.im and self.palette and self.palette.dirty:
|
if self.im is not None and self.palette and self.palette.dirty:
|
||||||
# realize palette
|
# realize palette
|
||||||
mode, arr = self.palette.getdata()
|
mode, arr = self.palette.getdata()
|
||||||
self.im.putpalette(mode, arr)
|
self.im.putpalette(mode, arr)
|
||||||
|
@ -864,7 +864,7 @@ class Image:
|
||||||
self.palette.mode = palette_mode
|
self.palette.mode = palette_mode
|
||||||
self.palette.palette = self.im.getpalette(palette_mode, palette_mode)
|
self.palette.palette = self.im.getpalette(palette_mode, palette_mode)
|
||||||
|
|
||||||
if self.im:
|
if self.im is not None:
|
||||||
if cffi and USE_CFFI_ACCESS:
|
if cffi and USE_CFFI_ACCESS:
|
||||||
if self.pyaccess:
|
if self.pyaccess:
|
||||||
return self.pyaccess
|
return self.pyaccess
|
||||||
|
@ -975,7 +975,9 @@ class Image:
|
||||||
delete_trns = False
|
delete_trns = False
|
||||||
# transparency handling
|
# transparency handling
|
||||||
if has_transparency:
|
if has_transparency:
|
||||||
if self.mode in ("1", "L", "I", "RGB") and mode == "RGBA":
|
if (self.mode in ("1", "L", "I") and mode in ("LA", "RGBA")) or (
|
||||||
|
self.mode == "RGB" and mode == "RGBA"
|
||||||
|
):
|
||||||
# Use transparent conversion to promote from transparent
|
# Use transparent conversion to promote from transparent
|
||||||
# color to an alpha channel.
|
# color to an alpha channel.
|
||||||
new_im = self._new(
|
new_im = self._new(
|
||||||
|
@ -1416,6 +1418,7 @@ class Image:
|
||||||
"".join(self.info["Raw profile type exif"].split("\n")[3:])
|
"".join(self.info["Raw profile type exif"].split("\n")[3:])
|
||||||
)
|
)
|
||||||
elif hasattr(self, "tag_v2"):
|
elif hasattr(self, "tag_v2"):
|
||||||
|
self._exif.bigtiff = self.tag_v2._bigtiff
|
||||||
self._exif.endian = self.tag_v2._endian
|
self._exif.endian = self.tag_v2._endian
|
||||||
self._exif.load_from_fp(self.fp, self.tag_v2._offset)
|
self._exif.load_from_fp(self.fp, self.tag_v2._offset)
|
||||||
if exif_info is not None:
|
if exif_info is not None:
|
||||||
|
@ -1492,11 +1495,12 @@ class Image:
|
||||||
|
|
||||||
def histogram(self, mask=None, extrema=None):
|
def histogram(self, mask=None, extrema=None):
|
||||||
"""
|
"""
|
||||||
Returns a histogram for the image. The histogram is returned as
|
Returns a histogram for the image. The histogram is returned as a
|
||||||
a list of pixel counts, one for each pixel value in the source
|
list of pixel counts, one for each pixel value in the source
|
||||||
image. If the image has more than one band, the histograms for
|
image. Counts are grouped into 256 bins for each band, even if
|
||||||
all bands are concatenated (for example, the histogram for an
|
the image has more than 8 bits per band. If the image has more
|
||||||
"RGB" image contains 768 values).
|
than one band, the histograms for all bands are concatenated (for
|
||||||
|
example, the histogram for an "RGB" image contains 768 values).
|
||||||
|
|
||||||
A bilevel image (mode "1") is treated as a greyscale ("L") image
|
A bilevel image (mode "1") is treated as a greyscale ("L") image
|
||||||
by this method.
|
by this method.
|
||||||
|
@ -1564,8 +1568,8 @@ class Image:
|
||||||
also use color strings as supported by the ImageColor module.
|
also use color strings as supported by the ImageColor module.
|
||||||
|
|
||||||
If a mask is given, this method updates only the regions
|
If a mask is given, this method updates only the regions
|
||||||
indicated by the mask. You can use either "1", "L" or "RGBA"
|
indicated by the mask. You can use either "1", "L", "LA", "RGBA"
|
||||||
images (in the latter case, the alpha band is used as mask).
|
or "RGBa" images (if present, the alpha band is used as mask).
|
||||||
Where the mask is 255, the given image is copied as is. Where
|
Where the mask is 255, the given image is copied as is. Where
|
||||||
the mask is 0, the current value is preserved. Intermediate
|
the mask is 0, the current value is preserved. Intermediate
|
||||||
values will mix the two images together, including their alpha
|
values will mix the two images together, including their alpha
|
||||||
|
@ -1613,7 +1617,7 @@ class Image:
|
||||||
elif isImageType(im):
|
elif isImageType(im):
|
||||||
im.load()
|
im.load()
|
||||||
if self.mode != im.mode:
|
if self.mode != im.mode:
|
||||||
if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"):
|
if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"):
|
||||||
# should use an adapter for this!
|
# should use an adapter for this!
|
||||||
im = im.convert(self.mode)
|
im = im.convert(self.mode)
|
||||||
im = im.im
|
im = im.im
|
||||||
|
@ -1716,6 +1720,8 @@ class Image:
|
||||||
# FIXME: _imaging returns a confusing error message for this case
|
# FIXME: _imaging returns a confusing error message for this case
|
||||||
raise ValueError("point operation not supported for this mode")
|
raise ValueError("point operation not supported for this mode")
|
||||||
|
|
||||||
|
if mode != "F":
|
||||||
|
lut = [round(i) for i in lut]
|
||||||
return self._new(self.im.point(lut, mode))
|
return self._new(self.im.point(lut, mode))
|
||||||
|
|
||||||
def putalpha(self, alpha):
|
def putalpha(self, alpha):
|
||||||
|
@ -2020,6 +2026,7 @@ class Image:
|
||||||
|
|
||||||
size = tuple(size)
|
size = tuple(size)
|
||||||
|
|
||||||
|
self.load()
|
||||||
if box is None:
|
if box is None:
|
||||||
box = (0, 0) + self.size
|
box = (0, 0) + self.size
|
||||||
else:
|
else:
|
||||||
|
@ -2282,7 +2289,9 @@ class Image:
|
||||||
else:
|
else:
|
||||||
save_handler = SAVE[format.upper()]
|
save_handler = SAVE[format.upper()]
|
||||||
|
|
||||||
|
created = False
|
||||||
if open_fp:
|
if open_fp:
|
||||||
|
created = not os.path.exists(filename)
|
||||||
if params.get("append", False):
|
if params.get("append", False):
|
||||||
# Open also for reading ("+"), because TIFF save_all
|
# Open also for reading ("+"), because TIFF save_all
|
||||||
# writer needs to go back and edit the written data.
|
# writer needs to go back and edit the written data.
|
||||||
|
@ -2292,10 +2301,17 @@ class Image:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
save_handler(self, fp, filename)
|
save_handler(self, fp, filename)
|
||||||
finally:
|
except Exception:
|
||||||
# do what we can to clean up
|
|
||||||
if open_fp:
|
if open_fp:
|
||||||
fp.close()
|
fp.close()
|
||||||
|
if created:
|
||||||
|
try:
|
||||||
|
os.remove(filename)
|
||||||
|
except PermissionError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
if open_fp:
|
||||||
|
fp.close()
|
||||||
|
|
||||||
def seek(self, frame):
|
def seek(self, frame):
|
||||||
"""
|
"""
|
||||||
|
@ -2435,6 +2451,7 @@ class Image:
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
self.load()
|
||||||
x, y = map(math.floor, size)
|
x, y = map(math.floor, size)
|
||||||
if x >= self.width and y >= self.height:
|
if x >= self.width and y >= self.height:
|
||||||
return
|
return
|
||||||
|
@ -2779,9 +2796,9 @@ def frombytes(mode, size, data, decoder_name="raw", *args):
|
||||||
In its simplest form, this function takes three arguments
|
In its simplest form, this function takes three arguments
|
||||||
(mode, size, and unpacked pixel data).
|
(mode, size, and unpacked pixel data).
|
||||||
|
|
||||||
You can also use any pixel decoder supported by PIL. For more
|
You can also use any pixel decoder supported by PIL. For more
|
||||||
information on available decoders, see the section
|
information on available decoders, see the section
|
||||||
:ref:`Writing Your Own File Decoder <file-decoders>`.
|
:ref:`Writing Your Own File Codec <file-codecs>`.
|
||||||
|
|
||||||
Note that this function decodes pixel data only, not entire images.
|
Note that this function decodes pixel data only, not entire images.
|
||||||
If you have an entire image in a string, wrap it in a
|
If you have an entire image in a string, wrap it in a
|
||||||
|
@ -3134,7 +3151,7 @@ def alpha_composite(im1, im2):
|
||||||
def blend(im1, im2, alpha):
|
def blend(im1, im2, alpha):
|
||||||
"""
|
"""
|
||||||
Creates a new image by interpolating between two input images, using
|
Creates a new image by interpolating between two input images, using
|
||||||
a constant alpha.::
|
a constant alpha::
|
||||||
|
|
||||||
out = image1 * (1.0 - alpha) + image2 * alpha
|
out = image1 * (1.0 - alpha) + image2 * alpha
|
||||||
|
|
||||||
|
@ -3423,6 +3440,7 @@ atexit.register(core.clear_cache)
|
||||||
|
|
||||||
class Exif(MutableMapping):
|
class Exif(MutableMapping):
|
||||||
endian = None
|
endian = None
|
||||||
|
bigtiff = False
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._data = {}
|
self._data = {}
|
||||||
|
@ -3458,10 +3476,15 @@ class Exif(MutableMapping):
|
||||||
return self._fixup_dict(info)
|
return self._fixup_dict(info)
|
||||||
|
|
||||||
def _get_head(self):
|
def _get_head(self):
|
||||||
|
version = b"\x2B" if self.bigtiff else b"\x2A"
|
||||||
if self.endian == "<":
|
if self.endian == "<":
|
||||||
return b"II\x2A\x00\x08\x00\x00\x00"
|
head = b"II" + version + b"\x00" + o32le(8)
|
||||||
else:
|
else:
|
||||||
return b"MM\x00\x2A\x00\x00\x00\x08"
|
head = b"MM\x00" + version + o32be(8)
|
||||||
|
if self.bigtiff:
|
||||||
|
head += o32le(8) if self.endian == "<" else o32be(8)
|
||||||
|
head += b"\x00\x00\x00\x00"
|
||||||
|
return head
|
||||||
|
|
||||||
def load(self, data):
|
def load(self, data):
|
||||||
# Extract EXIF information. This is highly experimental,
|
# Extract EXIF information. This is highly experimental,
|
||||||
|
@ -3475,12 +3498,12 @@ class Exif(MutableMapping):
|
||||||
self._loaded_exif = data
|
self._loaded_exif = data
|
||||||
self._data.clear()
|
self._data.clear()
|
||||||
self._ifds.clear()
|
self._ifds.clear()
|
||||||
|
if data and data.startswith(b"Exif\x00\x00"):
|
||||||
|
data = data[6:]
|
||||||
if not data:
|
if not data:
|
||||||
self._info = None
|
self._info = None
|
||||||
return
|
return
|
||||||
|
|
||||||
if data.startswith(b"Exif\x00\x00"):
|
|
||||||
data = data[6:]
|
|
||||||
self.fp = io.BytesIO(data)
|
self.fp = io.BytesIO(data)
|
||||||
self.head = self.fp.read(8)
|
self.head = self.fp.read(8)
|
||||||
# process dictionary
|
# process dictionary
|
||||||
|
|
|
@ -223,15 +223,15 @@ class ImageFile(Image.Image):
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
for decoder_name, extents, offset, args in self.tile:
|
for decoder_name, extents, offset, args in self.tile:
|
||||||
|
seek(offset)
|
||||||
decoder = Image._getdecoder(
|
decoder = Image._getdecoder(
|
||||||
self.mode, decoder_name, args, self.decoderconfig
|
self.mode, decoder_name, args, self.decoderconfig
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
seek(offset)
|
|
||||||
decoder.setimage(self.im, extents)
|
decoder.setimage(self.im, extents)
|
||||||
if decoder.pulls_fd:
|
if decoder.pulls_fd:
|
||||||
decoder.setfd(self.fp)
|
decoder.setfd(self.fp)
|
||||||
status, err_code = decoder.decode(b"")
|
err_code = decoder.decode(b"")[1]
|
||||||
else:
|
else:
|
||||||
b = prefix
|
b = prefix
|
||||||
while True:
|
while True:
|
||||||
|
@ -499,40 +499,33 @@ def _save(im, fp, tile, bufsize=0):
|
||||||
try:
|
try:
|
||||||
fh = fp.fileno()
|
fh = fp.fileno()
|
||||||
fp.flush()
|
fp.flush()
|
||||||
except (AttributeError, io.UnsupportedOperation) as exc:
|
exc = None
|
||||||
# compress to Python file-compatible object
|
except (AttributeError, io.UnsupportedOperation) as e:
|
||||||
for e, b, o, a in tile:
|
exc = e
|
||||||
e = Image._getencoder(im.mode, e, a, im.encoderconfig)
|
for e, b, o, a in tile:
|
||||||
if o > 0:
|
if o > 0:
|
||||||
fp.seek(o)
|
fp.seek(o)
|
||||||
e.setimage(im.im, b)
|
encoder = Image._getencoder(im.mode, e, a, im.encoderconfig)
|
||||||
if e.pushes_fd:
|
try:
|
||||||
e.setfd(fp)
|
encoder.setimage(im.im, b)
|
||||||
l, s = e.encode_to_pyfd()
|
if encoder.pushes_fd:
|
||||||
|
encoder.setfd(fp)
|
||||||
|
l, s = encoder.encode_to_pyfd()
|
||||||
else:
|
else:
|
||||||
while True:
|
if exc:
|
||||||
l, s, d = e.encode(bufsize)
|
# compress to Python file-compatible object
|
||||||
fp.write(d)
|
while True:
|
||||||
if s:
|
l, s, d = encoder.encode(bufsize)
|
||||||
break
|
fp.write(d)
|
||||||
|
if s:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# slight speedup: compress to real file object
|
||||||
|
s = encoder.encode_to_file(fh, bufsize)
|
||||||
if s < 0:
|
if s < 0:
|
||||||
raise OSError(f"encoder error {s} when writing image file") from exc
|
raise OSError(f"encoder error {s} when writing image file") from exc
|
||||||
e.cleanup()
|
finally:
|
||||||
else:
|
encoder.cleanup()
|
||||||
# slight speedup: compress to real file object
|
|
||||||
for e, b, o, a in tile:
|
|
||||||
e = Image._getencoder(im.mode, e, a, im.encoderconfig)
|
|
||||||
if o > 0:
|
|
||||||
fp.seek(o)
|
|
||||||
e.setimage(im.im, b)
|
|
||||||
if e.pushes_fd:
|
|
||||||
e.setfd(fp)
|
|
||||||
l, s = e.encode_to_pyfd()
|
|
||||||
else:
|
|
||||||
s = e.encode_to_file(fh, bufsize)
|
|
||||||
if s < 0:
|
|
||||||
raise OSError(f"encoder error {s} when writing image file")
|
|
||||||
e.cleanup()
|
|
||||||
if hasattr(fp, "flush"):
|
if hasattr(fp, "flush"):
|
||||||
fp.flush()
|
fp.flush()
|
||||||
|
|
||||||
|
@ -671,7 +664,7 @@ class PyDecoder(PyCodec):
|
||||||
|
|
||||||
:param buffer: A bytes object with the data to be decoded.
|
:param buffer: A bytes object with the data to be decoded.
|
||||||
:returns: A tuple of ``(bytes consumed, errcode)``.
|
:returns: A tuple of ``(bytes consumed, errcode)``.
|
||||||
If finished with decoding return 0 for the bytes consumed.
|
If finished with decoding return -1 for the bytes consumed.
|
||||||
Err codes are from :data:`.ImageFile.ERRORS`.
|
Err codes are from :data:`.ImageFile.ERRORS`.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
@ -725,6 +718,9 @@ class PyEncoder(PyCodec):
|
||||||
|
|
||||||
def encode_to_pyfd(self):
|
def encode_to_pyfd(self):
|
||||||
"""
|
"""
|
||||||
|
If ``pushes_fd`` is ``True``, then this method will be used,
|
||||||
|
and ``encode()`` will only be called once.
|
||||||
|
|
||||||
:returns: A tuple of ``(bytes consumed, errcode)``.
|
:returns: A tuple of ``(bytes consumed, errcode)``.
|
||||||
Err codes are from :data:`.ImageFile.ERRORS`.
|
Err codes are from :data:`.ImageFile.ERRORS`.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -30,14 +30,18 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
|
||||||
if sys.platform == "darwin":
|
if sys.platform == "darwin":
|
||||||
fh, filepath = tempfile.mkstemp(".png")
|
fh, filepath = tempfile.mkstemp(".png")
|
||||||
os.close(fh)
|
os.close(fh)
|
||||||
subprocess.call(["screencapture", "-x", filepath])
|
args = ["screencapture"]
|
||||||
|
if bbox:
|
||||||
|
left, top, right, bottom = bbox
|
||||||
|
args += ["-R", f"{left},{right},{right-left},{bottom-top}"]
|
||||||
|
subprocess.call(args + ["-x", filepath])
|
||||||
im = Image.open(filepath)
|
im = Image.open(filepath)
|
||||||
im.load()
|
im.load()
|
||||||
os.unlink(filepath)
|
os.unlink(filepath)
|
||||||
if bbox:
|
if bbox:
|
||||||
im_cropped = im.crop(bbox)
|
im_resized = im.resize((right - left, bottom - top))
|
||||||
im.close()
|
im.close()
|
||||||
return im_cropped
|
return im_resized
|
||||||
return im
|
return im
|
||||||
elif sys.platform == "win32":
|
elif sys.platform == "win32":
|
||||||
offset, size, data = Image.core.grabscreen_win32(
|
offset, size, data = Image.core.grabscreen_win32(
|
||||||
|
|
|
@ -525,7 +525,7 @@ def invert(image):
|
||||||
lut = []
|
lut = []
|
||||||
for i in range(256):
|
for i in range(256):
|
||||||
lut.append(255 - i)
|
lut.append(255 - i)
|
||||||
return _lut(image, lut)
|
return image.point(lut) if image.mode == "1" else _lut(image, lut)
|
||||||
|
|
||||||
|
|
||||||
def mirror(image):
|
def mirror(image):
|
||||||
|
|
|
@ -32,8 +32,6 @@ class ImagePalette:
|
||||||
an array or a list of ints between 0-255. The list must consist of
|
an array or a list of ints between 0-255. The list must consist of
|
||||||
all channels for one color followed by the next color (e.g. RGBRGBRGB).
|
all channels for one color followed by the next color (e.g. RGBRGBRGB).
|
||||||
Defaults to an empty palette.
|
Defaults to an empty palette.
|
||||||
:param size: An optional palette size. If given, an error is raised
|
|
||||||
if ``palette`` is not of equal length.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, mode="RGB", palette=None, size=0):
|
def __init__(self, mode="RGB", palette=None, size=0):
|
||||||
|
|
|
@ -270,8 +270,9 @@ class DisplayViewer(UnixViewer):
|
||||||
else:
|
else:
|
||||||
raise TypeError("Missing required argument: 'path'")
|
raise TypeError("Missing required argument: 'path'")
|
||||||
args = ["display"]
|
args = ["display"]
|
||||||
if "title" in options and options["title"] is not None:
|
title = options.get("title")
|
||||||
args += ["-title", options["title"]]
|
if title:
|
||||||
|
args += ["-title", title]
|
||||||
args.append(path)
|
args.append(path)
|
||||||
|
|
||||||
subprocess.Popen(args)
|
subprocess.Popen(args)
|
||||||
|
@ -368,8 +369,9 @@ class XVViewer(UnixViewer):
|
||||||
else:
|
else:
|
||||||
raise TypeError("Missing required argument: 'path'")
|
raise TypeError("Missing required argument: 'path'")
|
||||||
args = ["xv"]
|
args = ["xv"]
|
||||||
if "title" in options:
|
title = options.get("title")
|
||||||
args += ["-name", options["title"]]
|
if title:
|
||||||
|
args += ["-name", title]
|
||||||
args.append(path)
|
args.append(path)
|
||||||
|
|
||||||
subprocess.Popen(args)
|
subprocess.Popen(args)
|
||||||
|
|
|
@ -91,7 +91,7 @@ class Stat:
|
||||||
for i in range(0, len(self.h), 256):
|
for i in range(0, len(self.h), 256):
|
||||||
sum2 = 0.0
|
sum2 = 0.0
|
||||||
for j in range(256):
|
for j in range(256):
|
||||||
sum2 += (j ** 2) * float(self.h[i + j])
|
sum2 += (j**2) * float(self.h[i + j])
|
||||||
v.append(sum2)
|
v.append(sum2)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
import tkinter
|
import tkinter
|
||||||
|
import warnings
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from . import Image
|
from . import Image
|
||||||
|
@ -58,6 +59,33 @@ def _get_image_from_kw(kw):
|
||||||
return Image.open(source)
|
return Image.open(source)
|
||||||
|
|
||||||
|
|
||||||
|
def _pyimagingtkcall(command, photo, id):
|
||||||
|
tk = photo.tk
|
||||||
|
try:
|
||||||
|
tk.call(command, photo, id)
|
||||||
|
except tkinter.TclError:
|
||||||
|
# activate Tkinter hook
|
||||||
|
# may raise an error if it cannot attach to Tkinter
|
||||||
|
from . import _imagingtk
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(tk, "interp"):
|
||||||
|
# Required for PyPy, which always has CFFI installed
|
||||||
|
from cffi import FFI
|
||||||
|
|
||||||
|
ffi = FFI()
|
||||||
|
|
||||||
|
# PyPy is using an FFI CDATA element
|
||||||
|
# (Pdb) self.tk.interp
|
||||||
|
# <cdata 'Tcl_Interp *' 0x3061b50>
|
||||||
|
_imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1)
|
||||||
|
else:
|
||||||
|
_imagingtk.tkinit(tk.interpaddr(), 1)
|
||||||
|
except AttributeError:
|
||||||
|
_imagingtk.tkinit(id(tk), 0)
|
||||||
|
tk.call(command, photo, id)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# PhotoImage
|
# PhotoImage
|
||||||
|
|
||||||
|
@ -156,11 +184,15 @@ class PhotoImage:
|
||||||
:param im: A PIL image. The size must match the target region. If the
|
:param im: A PIL image. The size must match the target region. If the
|
||||||
mode does not match, the image is converted to the mode of
|
mode does not match, the image is converted to the mode of
|
||||||
the bitmap image.
|
the bitmap image.
|
||||||
:param box: A 4-tuple defining the left, upper, right, and lower pixel
|
|
||||||
coordinate. See :ref:`coordinate-system`. If None is given
|
|
||||||
instead of a tuple, all of the image is assumed.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if box is not None:
|
||||||
|
warnings.warn(
|
||||||
|
"The box parameter is deprecated and will be removed in Pillow 10 "
|
||||||
|
"(2023-07-01).",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
|
||||||
# convert to blittable
|
# convert to blittable
|
||||||
im.load()
|
im.load()
|
||||||
image = im.im
|
image = im.im
|
||||||
|
@ -170,33 +202,7 @@ class PhotoImage:
|
||||||
block = image.new_block(self.__mode, im.size)
|
block = image.new_block(self.__mode, im.size)
|
||||||
image.convert2(block, image) # convert directly between buffers
|
image.convert2(block, image) # convert directly between buffers
|
||||||
|
|
||||||
tk = self.__photo.tk
|
_pyimagingtkcall("PyImagingPhoto", self.__photo, block.id)
|
||||||
|
|
||||||
try:
|
|
||||||
tk.call("PyImagingPhoto", self.__photo, block.id)
|
|
||||||
except tkinter.TclError:
|
|
||||||
# activate Tkinter hook
|
|
||||||
try:
|
|
||||||
from . import _imagingtk
|
|
||||||
|
|
||||||
try:
|
|
||||||
if hasattr(tk, "interp"):
|
|
||||||
# Required for PyPy, which always has CFFI installed
|
|
||||||
from cffi import FFI
|
|
||||||
|
|
||||||
ffi = FFI()
|
|
||||||
|
|
||||||
# PyPy is using an FFI CDATA element
|
|
||||||
# (Pdb) self.tk.interp
|
|
||||||
# <cdata 'Tcl_Interp *' 0x3061b50>
|
|
||||||
_imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1)
|
|
||||||
else:
|
|
||||||
_imagingtk.tkinit(tk.interpaddr(), 1)
|
|
||||||
except AttributeError:
|
|
||||||
_imagingtk.tkinit(id(tk), 0)
|
|
||||||
tk.call("PyImagingPhoto", self.__photo, block.id)
|
|
||||||
except (ImportError, AttributeError, tkinter.TclError):
|
|
||||||
raise # configuration problem; cannot attach to Tkinter
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
@ -276,7 +282,7 @@ def getimage(photo):
|
||||||
im = Image.new("RGBA", (photo.width(), photo.height()))
|
im = Image.new("RGBA", (photo.width(), photo.height()))
|
||||||
block = im.im
|
block = im.im
|
||||||
|
|
||||||
photo.tk.call("PyImagingPhotoGet", photo, block.id)
|
_pyimagingtkcall("PyImagingPhotoGet", photo, block.id)
|
||||||
|
|
||||||
return im
|
return im
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ from . import Image, ImageFile
|
||||||
#
|
#
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
|
||||||
field = re.compile(br"([a-z]*) ([^ \r\n]*)")
|
field = re.compile(rb"([a-z]*) ([^ \r\n]*)")
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
|
@ -132,7 +132,7 @@ def _res_to_dpi(num, denom, exp):
|
||||||
calculated as (num / denom) * 10^exp and stored in dots per meter,
|
calculated as (num / denom) * 10^exp and stored in dots per meter,
|
||||||
to floating-point dots per inch."""
|
to floating-point dots per inch."""
|
||||||
if denom != 0:
|
if denom != 0:
|
||||||
return (254 * num * (10 ** exp)) / (10000 * denom)
|
return (254 * num * (10**exp)) / (10000 * denom)
|
||||||
|
|
||||||
|
|
||||||
def _parse_jp2_header(fp):
|
def _parse_jp2_header(fp):
|
||||||
|
@ -290,14 +290,14 @@ def _accept(prefix):
|
||||||
|
|
||||||
|
|
||||||
def _save(im, fp, filename):
|
def _save(im, fp, filename):
|
||||||
if filename.endswith(".j2k"):
|
# Get the keyword arguments
|
||||||
|
info = im.encoderinfo
|
||||||
|
|
||||||
|
if filename.endswith(".j2k") or info.get("no_jp2", False):
|
||||||
kind = "j2k"
|
kind = "j2k"
|
||||||
else:
|
else:
|
||||||
kind = "jp2"
|
kind = "jp2"
|
||||||
|
|
||||||
# Get the keyword arguments
|
|
||||||
info = im.encoderinfo
|
|
||||||
|
|
||||||
offset = info.get("offset", None)
|
offset = info.get("offset", None)
|
||||||
tile_offset = info.get("tile_offset", None)
|
tile_offset = info.get("tile_offset", None)
|
||||||
tile_size = info.get("tile_size", None)
|
tile_size = info.get("tile_size", None)
|
||||||
|
@ -320,6 +320,7 @@ def _save(im, fp, filename):
|
||||||
irreversible = info.get("irreversible", False)
|
irreversible = info.get("irreversible", False)
|
||||||
progression = info.get("progression", "LRCP")
|
progression = info.get("progression", "LRCP")
|
||||||
cinema_mode = info.get("cinema_mode", "no")
|
cinema_mode = info.get("cinema_mode", "no")
|
||||||
|
mct = info.get("mct", 0)
|
||||||
fd = -1
|
fd = -1
|
||||||
|
|
||||||
if hasattr(fp, "fileno"):
|
if hasattr(fp, "fileno"):
|
||||||
|
@ -340,6 +341,7 @@ def _save(im, fp, filename):
|
||||||
irreversible,
|
irreversible,
|
||||||
progression,
|
progression,
|
||||||
cinema_mode,
|
cinema_mode,
|
||||||
|
mct,
|
||||||
fd,
|
fd,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|