Merge branch 'main' into add-cygwin-to-ci
|
@ -25,8 +25,8 @@ install:
|
|||
- mv c:\pillow-depends-main c:\pillow-depends
|
||||
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
|
||||
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
|
||||
- ..\pillow-depends\gs9550w32.exe /S
|
||||
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.55.0\bin;%PATH%
|
||||
- ..\pillow-depends\gs9561w32.exe /S
|
||||
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH%
|
||||
- cd c:\pillow\winbuild\
|
||||
- ps: |
|
||||
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
||||
|
@ -43,7 +43,7 @@ build_script:
|
|||
|
||||
test_script:
|
||||
- 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%
|
||||
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
|
||||
- '%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
|
||||
dry-run: false
|
||||
- name: Upload New Crash
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure() && steps.build.outcome == 'success'
|
||||
with:
|
||||
name: artifacts
|
||||
path: ./out/artifacts
|
||||
- name: Upload Legacy Crash
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
if: steps.run.outcome == 'success'
|
||||
with:
|
||||
name: crash
|
||||
|
|
4
.github/workflows/lint.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
|||
name: Lint
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: pre-commit cache
|
||||
uses: actions/cache@v2
|
||||
|
@ -21,7 +21,7 @@ jobs:
|
|||
lint-pre-commit-
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
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,
|
||||
debian-10-buster-x86,
|
||||
debian-11-bullseye-x86,
|
||||
fedora-34-amd64,
|
||||
fedora-35-amd64,
|
||||
gentoo,
|
||||
ubuntu-18.04-bionic-amd64,
|
||||
|
@ -41,7 +40,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
|
2
.github/workflows/test-mingw.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up shell
|
||||
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
|
||||
|
|
2
.github/workflows/test-valgrind.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
|
14
.github/workflows/test-windows.yml
vendored
|
@ -23,17 +23,17 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Checkout cached dependencies
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: python-pillow/pillow-depends
|
||||
path: winbuild\depends
|
||||
|
||||
# sets env: pythonLocation
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
architecture: ${{ matrix.architecture }}
|
||||
|
@ -52,8 +52,8 @@ jobs:
|
|||
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
|
||||
|
||||
winbuild\depends\gs9550w32.exe /S
|
||||
echo "C:\Program Files (x86)\gs\gs9.55.0\bin" >> $env:GITHUB_PATH
|
||||
winbuild\depends\gs9561w32.exe /S
|
||||
echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH
|
||||
|
||||
xcopy /S /Y winbuild\depends\test_images\* Tests\images\
|
||||
|
||||
|
@ -156,7 +156,7 @@ jobs:
|
|||
shell: bash
|
||||
|
||||
- name: Upload errors
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: errors
|
||||
|
@ -182,7 +182,7 @@ jobs:
|
|||
winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
|
||||
shell: cmd
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: "github.event_name != 'pull_request'"
|
||||
with:
|
||||
name: ${{ steps.wheel.outputs.dist }}
|
||||
|
|
8
.github/workflows/test.yml
vendored
|
@ -36,10 +36,10 @@ jobs:
|
|||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: pip
|
||||
|
@ -84,7 +84,7 @@ jobs:
|
|||
mkdir -p Tests/errors
|
||||
|
||||
- name: Upload errors
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: errors
|
||||
|
@ -93,7 +93,7 @@ jobs:
|
|||
- name: Docs
|
||||
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
|
||||
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
|
||||
|
||||
- name: After success
|
||||
|
|
2
.github/workflows/tidelift.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Scan
|
||||
uses: tidelift/alignment-action@main
|
||||
env:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: f1d4e742c91dd5179d742b0db9293c4472b765f8 # frozen: 21.12b0
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ["--target-version", "py37"]
|
||||
|
@ -9,35 +9,35 @@ repos:
|
|||
types: []
|
||||
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: c5e8fa75dda5f764d20f66a215d71c21cfa198e1 # frozen: 5.10.1
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
- repo: https://github.com/asottile/yesqa
|
||||
rev: 35cf7dc24fa922927caded7a21b2a8cb04bf8e10 # frozen: v1.3.0
|
||||
rev: v1.3.0
|
||||
hooks:
|
||||
- id: yesqa
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10
|
||||
rev: v1.1.13
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: cbeb4c9c4137cff1568659fcc48e8b85cddd0c8d # frozen: 4.0.1
|
||||
rev: 4.0.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
||||
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0
|
||||
rev: v1.9.0
|
||||
hooks:
|
||||
- id: python-check-blanket-noqa
|
||||
- id: rst-backticks
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: 8fe62d14e0b4d7d845a7022c5c2c3ae41bdd3f26 # frozen: v4.1.0
|
||||
rev: v4.1.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
|
|
83
CHANGES.rst
|
@ -2,9 +2,90 @@
|
|||
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
|
||||
[Piolie, radarhere]
|
||||
|
||||
|
|
2
Makefile
|
@ -77,7 +77,7 @@ release-test:
|
|||
-rm dist/*.egg
|
||||
-rmdir dist
|
||||
python3 -m pytest -qq
|
||||
python3 -m check-manifest
|
||||
python3 -m check_manifest
|
||||
python3 -m pyroma .
|
||||
$(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:
|
||||
```bash
|
||||
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)
|
||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||
```bash
|
||||
twine check dist/*
|
||||
twine upload dist/Pillow-5.2.0*
|
||||
python3 -m twine check --strict dist/*
|
||||
python3 -m twine upload dist/Pillow-5.2.0*
|
||||
```
|
||||
* [ ] 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`
|
||||
|
@ -61,13 +61,13 @@ Released as needed for security, installation or critical bug fixes.
|
|||
* [ ] Create and check source distribution:
|
||||
```bash
|
||||
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)
|
||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||
```bash
|
||||
twine check dist/*
|
||||
twine upload dist/Pillow-5.2.1*
|
||||
python3 -m twine check --strict dist/*
|
||||
python3 -m twine upload dist/Pillow-5.2.1*
|
||||
```
|
||||
* [ ] 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:
|
||||
```bash
|
||||
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)
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||
|
|
|
@ -4,5 +4,5 @@ import sys
|
|||
|
||||
from PIL import Image
|
||||
|
||||
if sys.maxsize < 2 ** 32:
|
||||
if sys.maxsize < 2**32:
|
||||
im = Image.new("L", (999999, 999999), 0)
|
||||
|
|
|
@ -23,7 +23,7 @@ YDIM = 32769
|
|||
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):
|
||||
|
|
|
@ -19,7 +19,7 @@ YDIM = 32769
|
|||
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):
|
||||
|
|
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",
|
||||
"rgb24largepal.bmp",
|
||||
"pal8os2sp.bmp",
|
||||
"pal8rletrns.bmp",
|
||||
"rgb32bf-xbgr.bmp",
|
||||
]
|
||||
for f in get_files("q"):
|
||||
|
|
|
@ -110,9 +110,9 @@ class TestCoreMemory:
|
|||
|
||||
with pytest.raises(ValueError):
|
||||
Image.core.set_blocks_max(-1)
|
||||
if sys.maxsize < 2 ** 32:
|
||||
if sys.maxsize < 2**32:
|
||||
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")
|
||||
def test_set_blocks_max_stats(self):
|
||||
|
|
|
@ -4,7 +4,12 @@ import pytest
|
|||
|
||||
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):
|
||||
|
@ -125,6 +130,42 @@ def test_rgba_bitfields():
|
|||
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():
|
||||
# This image has been hexedited
|
||||
# to exclude the palette size from the pixel data offset
|
||||
|
|
|
@ -196,6 +196,13 @@ def test__accept_false():
|
|||
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():
|
||||
"""Check a short header"""
|
||||
with open(TEST_FILE_DXT5, "rb") as f:
|
||||
|
|
|
@ -16,6 +16,13 @@ def test_load_dxt1():
|
|||
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():
|
||||
for enum, prefix in {
|
||||
FtexImagePlugin.Format: "FORMAT_",
|
||||
|
|
|
@ -59,6 +59,51 @@ def test_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_grayscale(optimize):
|
||||
im = Image.new("L", (1, 1), 0)
|
||||
|
@ -383,18 +428,38 @@ def test_dispose_background_transparency():
|
|||
assert px[35, 30][3] == 0
|
||||
|
||||
|
||||
def test_transparent_dispose():
|
||||
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)),
|
||||
]
|
||||
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]
|
||||
@pytest.mark.parametrize(
|
||||
"loading_strategy, expected_colors",
|
||||
(
|
||||
(
|
||||
GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
|
||||
(
|
||||
(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_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():
|
||||
|
@ -831,6 +896,17 @@ def test_rgb_transparency(tmp_path):
|
|||
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):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
|
@ -960,6 +1036,11 @@ def test_lzw_bits():
|
|||
def test_extents():
|
||||
with Image.open("Tests/images/test_extents.gif") as im:
|
||||
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)
|
||||
assert im.size == (150, 150)
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import io
|
||||
import os
|
||||
|
||||
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"))
|
||||
def test_save_to_bytes_bmp(mode):
|
||||
output = io.BytesIO()
|
||||
|
|
|
@ -68,6 +68,13 @@ class TestFileJpeg:
|
|||
assert im.format == "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):
|
||||
# Test APP/COM reader (@PIL135)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
|
|
|
@ -209,6 +209,49 @@ def test_layers():
|
|||
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():
|
||||
# Arrange
|
||||
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
|
||||
|
|
|
@ -4,7 +4,6 @@ import itertools
|
|||
import os
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from ctypes import c_float
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -168,14 +167,11 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
val = original[tag]
|
||||
if tag.endswith("Resolution"):
|
||||
if legacy_api:
|
||||
assert (
|
||||
c_float(val[0][0] / val[0][1]).value
|
||||
== c_float(value[0][0] / value[0][1]).value
|
||||
assert val[0][0] / val[0][1] == (
|
||||
4294967295 / 113653537
|
||||
), f"{tag} didn't roundtrip"
|
||||
else:
|
||||
assert (
|
||||
c_float(val).value == c_float(value).value
|
||||
), f"{tag} didn't roundtrip"
|
||||
assert val == 37.79000115940079, f"{tag} didn't roundtrip"
|
||||
else:
|
||||
assert val == value, f"{tag} didn't roundtrip"
|
||||
|
||||
|
@ -218,7 +214,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
values = {
|
||||
2: "test",
|
||||
3: 1,
|
||||
4: 2 ** 20,
|
||||
4: 2**20,
|
||||
5: TiffImagePlugin.IFDRational(100, 1),
|
||||
12: 1.05,
|
||||
}
|
||||
|
@ -1019,7 +1015,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im = hopper("RGB").resize((256, 256))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
TiffImagePlugin.STRIP_SIZE = 2 ** 18
|
||||
TiffImagePlugin.STRIP_SIZE = 2**18
|
||||
try:
|
||||
|
||||
im.save(out, compression="tiff_adobe_deflate")
|
||||
|
|
|
@ -13,16 +13,53 @@ TEST_FILE = "Tests/images/hopper.ppm"
|
|||
|
||||
def test_sanity():
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.load()
|
||||
assert im.mode == "RGB"
|
||||
assert im.size == (128, 128)
|
||||
assert im.format, "PPM"
|
||||
assert im.format == "PPM"
|
||||
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():
|
||||
with Image.open("Tests/images/16_bit_binary.pgm") as im:
|
||||
im.load()
|
||||
assert im.mode == "I"
|
||||
assert im.size == (20, 100)
|
||||
assert im.get_format_mimetype() == "image/x-portable-graymap"
|
||||
|
@ -32,8 +69,6 @@ def test_16bit_pgm():
|
|||
|
||||
def test_16bit_pgm_write(tmp_path):
|
||||
with Image.open("Tests/images/16_bit_binary.pgm") as im:
|
||||
im.load()
|
||||
|
||||
f = str(tmp_path / "temp.pgm")
|
||||
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'"
|
||||
|
||||
|
||||
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):
|
||||
# Test EOF in header
|
||||
path = str(tmp_path / "temp.pgm")
|
||||
with open(path, "w") as f:
|
||||
f.write("P6")
|
||||
|
@ -114,6 +138,12 @@ def test_truncated_file(tmp_path):
|
|||
|
||||
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():
|
||||
# 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)
|
||||
|
||||
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(
|
||||
"file_name,mode,size,offset",
|
||||
[
|
||||
|
@ -221,6 +225,15 @@ class TestFileTiff:
|
|||
assert b[0] == ord(b"\x01")
|
||||
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):
|
||||
with Image.open("Tests/images/16bit.s.tif") as im:
|
||||
im.load()
|
||||
|
@ -598,6 +611,17 @@ class TestFileTiff:
|
|||
with Image.open(infile) as im:
|
||||
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 roundtrip(mode):
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
|
|
|
@ -258,7 +258,7 @@ def test_ifd_unsigned_rational(tmp_path):
|
|||
im = hopper()
|
||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
|
||||
max_long = 2 ** 32 - 1
|
||||
max_long = 2**32 - 1
|
||||
|
||||
# 4 bytes unsigned long
|
||||
numerator = max_long
|
||||
|
@ -290,8 +290,8 @@ def test_ifd_signed_rational(tmp_path):
|
|||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
|
||||
# pair of 4 byte signed longs
|
||||
numerator = 2 ** 31 - 1
|
||||
denominator = -(2 ** 31)
|
||||
numerator = 2**31 - 1
|
||||
denominator = -(2**31)
|
||||
|
||||
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 denominator == reloaded.tag_v2[37380].denominator
|
||||
|
||||
numerator = -(2 ** 31)
|
||||
denominator = 2 ** 31 - 1
|
||||
numerator = -(2**31)
|
||||
denominator = 2**31 - 1
|
||||
|
||||
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
||||
|
||||
|
@ -315,7 +315,7 @@ def test_ifd_signed_rational(tmp_path):
|
|||
assert denominator == reloaded.tag_v2[37380].denominator
|
||||
|
||||
# out of bounds of 4 byte signed long
|
||||
numerator = -(2 ** 31) - 1
|
||||
numerator = -(2**31) - 1
|
||||
denominator = 1
|
||||
|
||||
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
||||
|
@ -324,7 +324,7 @@ def test_ifd_signed_rational(tmp_path):
|
|||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import pytest
|
|||
from PIL import Image, WebPImagePlugin, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
assert_image_similar,
|
||||
assert_image_similar_tofile,
|
||||
hopper,
|
||||
|
@ -105,6 +106,19 @@ class TestFileWebp:
|
|||
hopper().save(buffer_method, format="WEBP", method=6)
|
||||
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):
|
||||
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
|
||||
if _webp.HAVE_WEBPANIM:
|
||||
|
@ -128,7 +142,7 @@ class TestFileWebp:
|
|||
|
||||
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):
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
im = Image.new("RGB", (15000, 15000))
|
||||
|
@ -171,9 +185,14 @@ class TestFileWebp:
|
|||
Image.open(blob).load()
|
||||
Image.open(blob).load()
|
||||
|
||||
@skip_unless_feature("webp")
|
||||
@skip_unless_feature("webp_anim")
|
||||
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:
|
||||
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))
|
||||
assert difference < 5
|
||||
|
||||
@skip_unless_feature("webp")
|
||||
@skip_unless_feature("webp_anim")
|
||||
def test_duration(self, tmp_path):
|
||||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -27,7 +28,6 @@ def test_n_frames():
|
|||
assert im.is_animated
|
||||
|
||||
|
||||
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
|
||||
def test_write_animation_L(tmp_path):
|
||||
"""
|
||||
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()
|
||||
im.load()
|
||||
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)
|
||||
im.seek(im.n_frames - 1)
|
||||
orig.load()
|
||||
|
@ -53,7 +58,6 @@ def test_write_animation_L(tmp_path):
|
|||
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):
|
||||
"""
|
||||
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"))
|
||||
|
||||
# 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.load()
|
||||
assert_image_equal(im, frame2.convert("RGBA"))
|
||||
|
|
|
@ -2,7 +2,7 @@ from io import BytesIO
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, XbmImagePlugin
|
||||
|
||||
from .helper import hopper
|
||||
|
||||
|
@ -63,6 +63,13 @@ def test_open_filename_with_underscore():
|
|||
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):
|
||||
im = hopper()
|
||||
out = str(tmp_path / "temp.xbm")
|
||||
|
|
|
@ -652,6 +652,15 @@ class TestImage:
|
|||
with warnings.catch_warnings():
|
||||
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):
|
||||
with open("Tests/images/frozenpond.mpo", "rb") as fp:
|
||||
|
||||
|
@ -666,6 +675,19 @@ class TestImage:
|
|||
|
||||
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(
|
||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||
)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import ctypes
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
@ -154,14 +153,17 @@ class TestImageGetPixel(AccessTest):
|
|||
|
||||
# Check 0
|
||||
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)
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.getpixel((0, 0))
|
||||
# Check 0 negative index
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.putpixel((-1, -1), c)
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.getpixel((-1, -1))
|
||||
|
||||
# check initial color
|
||||
|
@ -176,10 +178,10 @@ class TestImageGetPixel(AccessTest):
|
|||
|
||||
# Check 0
|
||||
im = Image.new(mode, (0, 0), c)
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.getpixel((0, 0))
|
||||
# Check 0 negative index
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.getpixel((-1, -1))
|
||||
|
||||
def test_basic(self):
|
||||
|
@ -205,10 +207,10 @@ class TestImageGetPixel(AccessTest):
|
|||
# see https://github.com/python-pillow/Pillow/issues/452
|
||||
# pixelaccess is using signed int* instead of uint*
|
||||
for mode in ("I;16", "I;16B"):
|
||||
self.check(mode, 2 ** 15 - 1)
|
||||
self.check(mode, 2 ** 15)
|
||||
self.check(mode, 2 ** 15 + 1)
|
||||
self.check(mode, 2 ** 16 - 1)
|
||||
self.check(mode, 2**15 - 1)
|
||||
self.check(mode, 2**15)
|
||||
self.check(mode, 2**15 + 1)
|
||||
self.check(mode, 2**16 - 1)
|
||||
|
||||
def test_p_putpixel_rgb_rgba(self):
|
||||
for color in [(255, 0, 0), (255, 0, 0, 255)]:
|
||||
|
@ -386,7 +388,7 @@ class TestImagePutPixelError(AccessTest):
|
|||
def test_putpixel_overflow_error(self, mode):
|
||||
im = hopper(mode)
|
||||
with pytest.raises(OverflowError):
|
||||
im.putpixel((0, 0), 2 ** 80)
|
||||
im.putpixel((0, 0), 2**80)
|
||||
|
||||
def test_putpixel_unrecognized_mode(self):
|
||||
im = hopper("BGR;15")
|
||||
|
@ -401,6 +403,8 @@ class TestEmbeddable:
|
|||
"not from shell",
|
||||
)
|
||||
def test_embeddable(self):
|
||||
import ctypes
|
||||
|
||||
with open("embed_pil.c", "w") as fh:
|
||||
fh.write(
|
||||
"""
|
||||
|
|
|
@ -70,6 +70,11 @@ def test_16bit():
|
|||
with Image.open("Tests/images/16bit.cropped.tif") as 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():
|
||||
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")
|
||||
|
||||
im_la = im.convert("LA")
|
||||
assert "transparency" not in im_la.info
|
||||
im_la.save(f)
|
||||
|
||||
im_rgb = im.convert("RGB")
|
||||
assert im_rgb.info["transparency"] == (128, 128, 128) # undone
|
||||
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
|
||||
def gradient_RGBA(self):
|
||||
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):
|
||||
for mode in ("RGBA", "RGB", "L"):
|
||||
im = Image.new(mode, (200, 200), "white")
|
||||
|
|
|
@ -10,6 +10,7 @@ def test_sanity():
|
|||
im.point(list(range(256)))
|
||||
im.point(list(range(256)) * 3)
|
||||
im.point(lambda x: x)
|
||||
im.point(lambda x: x * 1.2)
|
||||
|
||||
im = im.convert("I")
|
||||
with pytest.raises(ValueError):
|
||||
|
|
|
@ -38,7 +38,7 @@ def test_long_integers():
|
|||
assert put(0xFFFFFFFF) == (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)
|
||||
else:
|
||||
assert put(sys.maxsize) == (255, 255, 255, 127)
|
||||
|
|
|
@ -264,6 +264,13 @@ class TestImageResize:
|
|||
with pytest.raises(ValueError):
|
||||
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):
|
||||
for mode in "L", "RGB", "I", "F":
|
||||
im = hopper(mode)
|
||||
|
|
|
@ -88,6 +88,14 @@ def test_no_resize():
|
|||
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
|
||||
@pytest.mark.valgrind_known_error(reason="Known Failing")
|
||||
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:
|
||||
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):
|
||||
# Helper function to reduce precision of tuples of floats
|
||||
# recursively and then check equality.
|
||||
power = 10 ** digits
|
||||
power = 10**digits
|
||||
|
||||
def truncate_tuple(tuple_or_float):
|
||||
return tuple(
|
||||
|
|
|
@ -1440,3 +1440,15 @@ def test_continuous_horizontal_edges_polygon():
|
|||
assert_image_equal_tofile(
|
||||
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):
|
||||
return 1, 1, b""
|
||||
|
||||
def cleanup(self):
|
||||
self.cleanup_called = True
|
||||
|
||||
|
||||
xoff, yoff, xsize, ysize = 10, 20, 100, 100
|
||||
|
||||
|
@ -327,10 +330,12 @@ class TestPyEncoder(CodecsTest):
|
|||
im = MockImageFile(buf)
|
||||
|
||||
fp = BytesIO()
|
||||
self.encoder.cleanup_called = False
|
||||
with pytest.raises(ValueError):
|
||||
ImageFile._save(
|
||||
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
|
||||
)
|
||||
assert self.encoder.cleanup_called
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ImageFile._save(
|
||||
|
|
|
@ -48,10 +48,6 @@ def img_string_normalize(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):
|
||||
assert img_to_string(A) == img_string_normalize(Bstring)
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ def test_sanity():
|
|||
ImageOps.grayscale(hopper("L"))
|
||||
ImageOps.grayscale(hopper("RGB"))
|
||||
|
||||
ImageOps.invert(hopper("1"))
|
||||
ImageOps.invert(hopper("L"))
|
||||
ImageOps.invert(hopper("RGB"))
|
||||
|
||||
|
|
|
@ -32,10 +32,10 @@ def test_rgb():
|
|||
|
||||
def checkrgb(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 >> 8) % 2 ** 8) == g
|
||||
assert val % 2 ** 8 == b
|
||||
assert ((val >> 8) % 2**8) == g
|
||||
assert val % 2**8 == b
|
||||
|
||||
checkrgb(0, 0, 0)
|
||||
checkrgb(255, 0, 0)
|
||||
|
|
|
@ -51,8 +51,8 @@ def test_constant():
|
|||
st = ImageStat.Stat(im)
|
||||
|
||||
assert st.extrema[0] == (128, 128)
|
||||
assert st.sum[0] == 128 ** 3
|
||||
assert st.sum2[0] == 128 ** 4
|
||||
assert st.sum[0] == 128**3
|
||||
assert st.sum2[0] == 128**4
|
||||
assert st.mean[0] == 128
|
||||
assert st.median[0] == 128
|
||||
assert st.rms[0] == 128
|
||||
|
|
|
@ -75,8 +75,16 @@ def test_photoimage_blank():
|
|||
assert im_tk.width() == 100
|
||||
assert im_tk.height() == 100
|
||||
|
||||
# reloaded = ImageTk.getimage(im_tk)
|
||||
# assert_image_equal(reloaded, im)
|
||||
im = Image.new(mode, (100, 100))
|
||||
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():
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import ctypes
|
||||
from io import BytesIO
|
||||
|
||||
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
|
||||
|
||||
if is_win32():
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
|
||||
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;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", "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(
|
||||
"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
|
||||
|
||||
|
||||
@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():
|
||||
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(False) == b"false"
|
||||
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(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.
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
import sphinx_rtd_theme
|
||||
|
||||
import PIL
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
@ -126,13 +124,15 @@ nitpicky = True
|
|||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
html_theme = "furo"
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
# 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
|
||||
# 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
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
|
@ -311,10 +311,7 @@ texinfo_documents = [
|
|||
|
||||
|
||||
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/light.css")
|
||||
|
||||
|
||||
# GitHub repo for sphinx-issues
|
||||
|
|
|
@ -69,7 +69,7 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
|
|||
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
|
||||
(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
|
||||
: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
|
||||
----------------
|
||||
|
||||
|
|
|
@ -210,7 +210,9 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
format_description = "DirectDraw Surface"
|
||||
|
||||
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:
|
||||
raise OSError(f"Unsupported header size {repr(header_size)}")
|
||||
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))
|
||||
except struct.error as e:
|
||||
raise OSError("Truncated DDS file") from e
|
||||
return 0, 0
|
||||
return -1, 0
|
||||
|
||||
|
||||
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))
|
||||
except struct.error as e:
|
||||
raise OSError("Truncated DDS file") from e
|
||||
return 0, 0
|
||||
return -1, 0
|
||||
|
||||
|
||||
Image.register_decoder("DXT1", DXT1Decoder)
|
||||
|
|
|
@ -24,8 +24,6 @@ attribute will be ``None``.
|
|||
Fully supported formats
|
||||
-----------------------
|
||||
|
||||
.. contents::
|
||||
|
||||
BLP
|
||||
^^^
|
||||
|
||||
|
@ -44,8 +42,9 @@ BMP
|
|||
^^^
|
||||
|
||||
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
|
||||
is not supported.
|
||||
or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length encoding
|
||||
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
|
||||
: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.
|
||||
|
||||
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
|
||||
``RGB`` or ``RGBA``, depending on whether the first frame had transparency.
|
||||
images. Seeking to later frames in a ``P`` image will change the image to
|
||||
``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
|
||||
: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:
|
||||
|
||||
**quality**
|
||||
The image quality, on a scale from 0 (worst) to 95 (best). The default is
|
||||
75. Values above 95 should be avoided; 100 disables portions of the JPEG
|
||||
compression algorithm, and results in large files with hardly any gain in
|
||||
image quality.
|
||||
The image quality, on a scale from 0 (worst) to 95 (best), or the string
|
||||
``keep``. The default is 75. Values above 95 should be avoided; 100 disables
|
||||
portions of the JPEG compression algorithm, and results in large files with
|
||||
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**
|
||||
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.
|
||||
|
||||
**irreversible**
|
||||
If ``True``, use the lossy Irreversible Color Transformation
|
||||
followed by DWT 9-7. Defaults to ``False``, which means to use the
|
||||
Reversible Color Transformation with DWT 5-3.
|
||||
If ``True``, use the lossy discrete waveform transformation DWT 9-7.
|
||||
Defaults to ``False``, which uses the lossless 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**
|
||||
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
|
||||
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::
|
||||
|
||||
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
|
||||
^^^
|
||||
|
||||
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.
|
||||
|
||||
SGI
|
||||
|
|
|
@ -171,20 +171,37 @@ Rolling an image
|
|||
|
||||
::
|
||||
|
||||
def roll(image, delta):
|
||||
def roll(im, delta):
|
||||
"""Roll an image sideways."""
|
||||
xsize, ysize = image.size
|
||||
xsize, ysize = im.size
|
||||
|
||||
delta = delta % xsize
|
||||
if delta == 0:
|
||||
return image
|
||||
return im
|
||||
|
||||
part1 = image.crop((0, 0, delta, ysize))
|
||||
part2 = image.crop((delta, 0, xsize, ysize))
|
||||
image.paste(part1, (xsize - delta, 0, xsize, ysize))
|
||||
image.paste(part2, (0, 0, xsize - delta, ysize))
|
||||
part1 = im.crop((0, 0, delta, ysize))
|
||||
part2 = im.crop((delta, 0, xsize, ysize))
|
||||
im.paste(part1, (xsize - delta, 0, xsize, 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
|
||||
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``
|
||||
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
|
||||
given region in the image. In most cases, only a single descriptor is used,
|
||||
covering the full image.
|
||||
given region in the 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::
|
||||
|
||||
|
@ -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
|
||||
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
|
||||
back to a function named ``[decodername]_decoder`` on the internal
|
||||
core image object. That function is called with the ``args`` tuple
|
||||
from the ``tile`` setup in the ``_open`` method.
|
||||
1. Setup: Pillow looks for a function in the decoder or encoder registry,
|
||||
falling back to a function named ``[codecname]_decoder`` or
|
||||
``[codecname]_encoder`` on the internal core image object. That function is
|
||||
called with the ``args`` tuple from the ``tile``.
|
||||
|
||||
2. Decoding: The decoder's decode function is repeatedly called with
|
||||
chunks of image data.
|
||||
2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly
|
||||
called with chunks of image data.
|
||||
|
||||
3. Cleanup: If the decoder has registered a cleanup function, it will
|
||||
be called at the end of the decoding process, even if there was an
|
||||
3. Cleanup: If the codec has registered a cleanup function, it will
|
||||
be called at the end of the transformation process, even if there was an
|
||||
exception raised.
|
||||
|
||||
|
||||
Setup
|
||||
-----
|
||||
|
||||
The current conventions are that the decoder setup function is named
|
||||
``PyImaging_[Decodername]DecoderNew`` and defined in ``decode.c``. The
|
||||
python binding for it is named ``[decodername]_decoder`` and is setup
|
||||
from within the ``_imaging.c`` file in the codecs section of the
|
||||
function array.
|
||||
The current conventions are that the codec setup function is named
|
||||
``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew``
|
||||
and defined in ``decode.c`` or ``encode.c``. The Python binding for it is
|
||||
named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from
|
||||
within the ``_imaging.c`` file in the codecs section of the function array.
|
||||
|
||||
The setup function needs to call ``PyImaging_DecoderNew`` and at the
|
||||
very least, set the ``decode`` function pointer. The fields of
|
||||
interest in this object are:
|
||||
The setup function needs to call ``PyImaging_DecoderNew`` or
|
||||
``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or
|
||||
``encode`` function pointer. The fields of interest in this object are:
|
||||
|
||||
**decode**
|
||||
Function pointer to the decode function, which has access to
|
||||
``im``, ``state``, and the buffer of data to be added to the image.
|
||||
**decode**/**encode**
|
||||
Function pointer to the decode or encode function, which has access to
|
||||
``im``, ``state``, and the buffer of data to be transformed.
|
||||
|
||||
**cleanup**
|
||||
Function pointer to the cleanup function, has access to ``state``.
|
||||
|
@ -369,36 +373,34 @@ interest in this object are:
|
|||
|
||||
**state**
|
||||
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.
|
||||
|
||||
**pulls_fd**
|
||||
**EXPERIMENTAL** -- **WARNING**, interface may change. If set to 1,
|
||||
``state->fd`` will be a pointer to the Python file like object. The
|
||||
decoder may use the functions in ``codec_fd.c`` to read directly
|
||||
from the file like object rather than have the data pushed through a
|
||||
buffer. Note that this implementation may be refactored until this
|
||||
warning is removed.
|
||||
**pulls_fd**/**pushes_fd**
|
||||
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 codec may
|
||||
use the functions in ``codec_fd.c`` to read or write directly with the file
|
||||
like object rather than have the data pushed through a buffer.
|
||||
|
||||
.. versionadded:: 3.3.0
|
||||
|
||||
|
||||
Decoding
|
||||
--------
|
||||
Transforming
|
||||
------------
|
||||
|
||||
The decode function is called with the target (core) image, the
|
||||
decoder state structure, and a buffer of data to be decoded.
|
||||
The decode or encode function is called with the target (core) image, the codec
|
||||
state structure, and a buffer of data to be transformed.
|
||||
|
||||
**Experimental** -- If ``pulls_fd`` is set, then the decode function
|
||||
is called once, with an empty buffer. It is the decoder's
|
||||
responsibility to decode the entire tile in that one call. The rest of
|
||||
this section only applies if ``pulls_fd`` is not set.
|
||||
It is the codec's responsibility to pull as much data as possible out of the
|
||||
buffer and return the number of bytes consumed. The next call to the codec will
|
||||
include the previous unconsumed tail. The codec function will be called
|
||||
multiple times as the data processed.
|
||||
|
||||
It is the decoder's responsibility to pull as much data as possible
|
||||
out of the buffer and return the number of bytes consumed. The next
|
||||
call to the decoder will include the previous unconsumed tail. The
|
||||
decoder function will be called multiple times as the data is read
|
||||
from the file like object.
|
||||
Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or
|
||||
encode function is called once, with an empty buffer. It is the codec's
|
||||
responsibility to transform the entire tile in that one call. Using this will
|
||||
provide a codec with more freedom, but that freedom may mean increased memory
|
||||
usage if the entire tile is held in memory at once by the codec.
|
||||
|
||||
If an error occurs, set ``state->errcode`` and return -1.
|
||||
|
||||
|
@ -407,10 +409,9 @@ Return -1 on success, without setting the errcode.
|
|||
Cleanup
|
||||
-------
|
||||
|
||||
The cleanup function is called after the decoder returns a negative
|
||||
value, or if there is a read error from the file. This function should
|
||||
free any allocated memory and release any resources from external
|
||||
libraries.
|
||||
The cleanup function is called after the codec returns a negative
|
||||
value, or if there is an error. This function should free any allocated
|
||||
memory and release any resources from external libraries.
|
||||
|
||||
.. _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
|
||||
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.
|
||||
|
||||
2. Transforming: The instance's ``decode`` method is repeatedly called with
|
||||
a buffer of data to be interpreted, or the ``encode`` method is repeatedly
|
||||
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 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 34 | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 35 | 3.10 | 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.
|
||||
|
||||
: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.
|
||||
|
||||
.. py:attribute:: extrema
|
||||
|
|
|
@ -6,7 +6,13 @@
|
|||
The PixelAccess class provides read and write access to
|
||||
: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
|
||||
-------
|
||||
|
@ -39,7 +45,7 @@ Access using negative indexes is also possible.
|
|||
|
||||
|
||||
:py:class:`PixelAccess` Class
|
||||
-----------------------------------
|
||||
-----------------------------
|
||||
|
||||
.. 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.
|
||||
|
||||
.. 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.
|
||||
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
|
||||
-------
|
||||
|
|
|
@ -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
|
||||
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.
|
||||
A ``rawmode`` argument has been added, to allow the mode to be chosen instead. ``None``
|
||||
can be used to return data in the current mode of the palette.
|
||||
The :py:meth:`PIL.Image.Image.save` method now supports the following options for
|
||||
JPEG 2000:
|
||||
|
||||
**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
|
||||
^^^^^^^^^^^^^^^
|
||||
|
@ -160,9 +172,35 @@ Added PyEncoder
|
|||
written in Python. See :ref:`Writing Your Own File Codec in Python<file-codecs-py>` for
|
||||
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
|
||||
=============
|
||||
|
||||
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
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
@ -177,6 +215,11 @@ Image._repr_pretty_
|
|||
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.
|
||||
|
||||
Added BigTIFF reading
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Support has been added for reading BigTIFF images.
|
||||
|
||||
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]
|
||||
docs =
|
||||
furo
|
||||
olefile
|
||||
sphinx>=2.4
|
||||
sphinx-copybutton
|
||||
sphinx-issues>=3.0.1
|
||||
sphinx-removed-in
|
||||
sphinx-rtd-theme>=1.0
|
||||
sphinxext-opengraph
|
||||
tests =
|
||||
check-manifest
|
||||
|
|
2
setup.py
|
@ -167,7 +167,7 @@ def _find_library_dirs_ldconfig():
|
|||
# Assuming GLIBC's ldconfig (with option -p)
|
||||
# Alpine Linux uses musl that can't print cache
|
||||
args = ["/sbin/ldconfig", "-p"]
|
||||
expr = fr".*\({abi_type}.*\) => (.*)"
|
||||
expr = rf".*\({abi_type}.*\) => (.*)"
|
||||
env = dict(os.environ)
|
||||
env["LC_ALL"] = "C"
|
||||
env["LANG"] = "C"
|
||||
|
|
|
@ -306,7 +306,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
|||
self._load()
|
||||
except struct.error as e:
|
||||
raise OSError("Truncated BLP file") from e
|
||||
return 0, 0
|
||||
return -1, 0
|
||||
|
||||
def _read_blp_header(self):
|
||||
self.fd.seek(4)
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
#
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i16le as i16
|
||||
from ._binary import i32le as i32
|
||||
|
@ -102,7 +104,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
file_info["height"] = (
|
||||
i32(header_data, 4)
|
||||
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["bits"] = i16(header_data, 10)
|
||||
|
@ -167,6 +169,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})")
|
||||
|
||||
# ---------------- Process BMP with Bitfields compression (not palette)
|
||||
decoder_name = "raw"
|
||||
if file_info["compression"] == self.BITFIELDS:
|
||||
SUPPORTED = {
|
||||
32: [
|
||||
|
@ -208,6 +211,8 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
elif file_info["compression"] == self.RAW:
|
||||
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
|
||||
raw_mode, self.mode = "BGRA", "RGBA"
|
||||
elif file_info["compression"] == self.RLE8:
|
||||
decoder_name = "bmp_rle"
|
||||
else:
|
||||
raise OSError(f"Unsupported BMP compression ({file_info['compression']})")
|
||||
|
||||
|
@ -247,7 +252,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
self.info["compression"] = file_info["compression"]
|
||||
self.tile = [
|
||||
(
|
||||
"raw",
|
||||
decoder_name,
|
||||
(0, 0, file_info["width"], file_info["height"]),
|
||||
offset or self.fp.tell(),
|
||||
(
|
||||
|
@ -271,6 +276,57 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
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)
|
||||
# =============================================================================
|
||||
|
@ -322,7 +378,7 @@ def _save(im, fp, filename, bitmap_header=True):
|
|||
if bitmap_header:
|
||||
offset = 14 + header + colors * 4
|
||||
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")
|
||||
fp.write(
|
||||
b"BM" # file type (magic)
|
||||
|
@ -372,6 +428,8 @@ Image.register_extension(BmpImageFile.format, ".bmp")
|
|||
|
||||
Image.register_mime(BmpImageFile.format, "image/bmp")
|
||||
|
||||
Image.register_decoder("bmp_rle", BmpRleDecoder)
|
||||
|
||||
Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
|
||||
Image.register_save(DibImageFile.format, _dib_save)
|
||||
|
||||
|
|
|
@ -111,7 +111,9 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
format_description = "DirectDraw Surface"
|
||||
|
||||
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:
|
||||
raise OSError(f"Unsupported header size {repr(header_size)}")
|
||||
header_bytes = self.fp.read(header_size - 4)
|
||||
|
|
|
@ -26,7 +26,11 @@ from ._binary import o8
|
|||
|
||||
|
||||
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
|
||||
s = self.fp.read(128)
|
||||
if not (
|
||||
_accept(s)
|
||||
and i16(s, 14) in [0, 3] # flags
|
||||
and s[20:22] == b"\x00\x00" # reserved
|
||||
):
|
||||
if not (_accept(s) and s[20:22] == b"\x00\x00"):
|
||||
raise SyntaxError("not an FLI/FLC file")
|
||||
|
||||
# frames
|
||||
|
|
|
@ -94,7 +94,8 @@ class FtexImageFile(ImageFile.ImageFile):
|
|||
format_description = "Texture File Format (IW2:EOC)"
|
||||
|
||||
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
|
||||
self._size = 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):
|
||||
header_size = i32(self.fp.read(4))
|
||||
version = i32(self.fp.read(4))
|
||||
if header_size < 20:
|
||||
raise SyntaxError("not a GIMP brush")
|
||||
version = i32(self.fp.read(4))
|
||||
if version not in (1, 2):
|
||||
raise SyntaxError(f"Unsupported GIMP brush version: {version}")
|
||||
|
||||
|
|
|
@ -28,12 +28,25 @@ import itertools
|
|||
import math
|
||||
import os
|
||||
import subprocess
|
||||
from enum import IntEnum
|
||||
|
||||
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
|
||||
from ._binary import i16le as i16
|
||||
from ._binary import o8
|
||||
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
|
||||
|
||||
|
@ -61,6 +74,12 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
return self.fp.read(s[0])
|
||||
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):
|
||||
|
||||
# Screen
|
||||
|
@ -79,11 +98,9 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
self.info["background"] = s[11]
|
||||
# check if palette contains colour indices
|
||||
p = self.fp.read(3 << bits)
|
||||
for i in range(0, len(p), 3):
|
||||
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
|
||||
p = ImagePalette.raw("RGB", p)
|
||||
self.global_palette = self.palette = p
|
||||
break
|
||||
if self._is_palette_needed(p):
|
||||
p = ImagePalette.raw("RGB", p)
|
||||
self.global_palette = self.palette = p
|
||||
|
||||
self.__fp = self.fp # FIXME: hack
|
||||
self.__rewind = self.fp.tell()
|
||||
|
@ -97,7 +114,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
current = self.tell()
|
||||
try:
|
||||
while True:
|
||||
self.seek(self.tell() + 1)
|
||||
self._seek(self.tell() + 1, False)
|
||||
except EOFError:
|
||||
self._n_frames = self.tell() + 1
|
||||
self.seek(current)
|
||||
|
@ -110,14 +127,16 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
self._is_animated = self._n_frames != 1
|
||||
else:
|
||||
current = self.tell()
|
||||
|
||||
try:
|
||||
self.seek(1)
|
||||
if current:
|
||||
self._is_animated = True
|
||||
except EOFError:
|
||||
self._is_animated = False
|
||||
else:
|
||||
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
|
||||
|
||||
def seek(self, frame):
|
||||
|
@ -135,26 +154,22 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
self.seek(last_frame)
|
||||
raise EOFError("no more images in GIF file") from e
|
||||
|
||||
def _seek(self, frame):
|
||||
def _seek(self, frame, update_image=True):
|
||||
|
||||
if frame == 0:
|
||||
# rewind
|
||||
self.__offset = 0
|
||||
self.dispose = None
|
||||
self.dispose_extent = [0, 0, 0, 0] # x0, y0, x1, y1
|
||||
self.__frame = -1
|
||||
self.__fp.seek(self.__rewind)
|
||||
self.disposal_method = 0
|
||||
else:
|
||||
# ensure that the previous frame was loaded
|
||||
if self.tile:
|
||||
if self.tile and update_image:
|
||||
self.load()
|
||||
|
||||
if frame != self.__frame + 1:
|
||||
raise ValueError(f"cannot seek to frame {frame}")
|
||||
self.__frame = frame
|
||||
|
||||
self.tile = []
|
||||
|
||||
self.fp = self.__fp
|
||||
if self.__offset:
|
||||
|
@ -164,28 +179,24 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
pass
|
||||
self.__offset = 0
|
||||
|
||||
if self.__frame == 1:
|
||||
self.pyaccess = None
|
||||
if "transparency" in self.info:
|
||||
self.mode = "RGBA"
|
||||
self.im.putpalettealpha(self.info["transparency"], 0)
|
||||
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
|
||||
s = self.fp.read(1)
|
||||
if not s or s == b";":
|
||||
raise EOFError
|
||||
|
||||
del self.info["transparency"]
|
||||
else:
|
||||
self.mode = "RGB"
|
||||
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
|
||||
if self.dispose:
|
||||
self.im.paste(self.dispose, self.dispose_extent)
|
||||
self.__frame = frame
|
||||
|
||||
self.tile = []
|
||||
|
||||
palette = None
|
||||
|
||||
info = {}
|
||||
frame_transparency = None
|
||||
interlace = None
|
||||
frame_dispose_extent = None
|
||||
while True:
|
||||
|
||||
s = self.fp.read(1)
|
||||
if not s:
|
||||
s = self.fp.read(1)
|
||||
if not s or s == b";":
|
||||
break
|
||||
|
||||
|
@ -223,6 +234,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
else:
|
||||
info["comment"] = block
|
||||
block = self.data()
|
||||
s = None
|
||||
continue
|
||||
elif s[0] == 255:
|
||||
#
|
||||
|
@ -245,16 +257,18 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
# extent
|
||||
x0, y0 = i16(s, 0), i16(s, 2)
|
||||
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.dispose_extent = x0, y0, x1, y1
|
||||
frame_dispose_extent = x0, y0, x1, y1
|
||||
flags = s[8]
|
||||
|
||||
interlace = (flags & 64) != 0
|
||||
|
||||
if flags & 128:
|
||||
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
|
||||
bits = self.fp.read(1)[0]
|
||||
|
@ -264,16 +278,56 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
else:
|
||||
pass
|
||||
# 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):
|
||||
if frame_palette:
|
||||
color = tuple(frame_palette.palette[color * 3 : color * 3 + 3])
|
||||
if self._frame_palette:
|
||||
color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
|
||||
else:
|
||||
color = (color, color, color)
|
||||
return color
|
||||
|
||||
self.dispose_extent = frame_dispose_extent
|
||||
try:
|
||||
if self.disposal_method < 2:
|
||||
# do not dispose or none specified
|
||||
|
@ -288,17 +342,21 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
Image._decompression_bomb_check(dispose_size)
|
||||
|
||||
# by convention, attempt to use transparency first
|
||||
dispose_mode = "P"
|
||||
color = self.info.get("transparency", frame_transparency)
|
||||
if color is not None:
|
||||
dispose_mode = "RGBA"
|
||||
color = _rgb(color) + (0,)
|
||||
if self.mode in ("RGB", "RGBA"):
|
||||
dispose_mode = "RGBA"
|
||||
color = _rgb(color) + (0,)
|
||||
else:
|
||||
dispose_mode = "RGB"
|
||||
color = _rgb(self.info.get("background", 0))
|
||||
color = 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)
|
||||
else:
|
||||
# replace with previous contents
|
||||
if self.im:
|
||||
if self.im is not None:
|
||||
# only dispose the extent in this frame
|
||||
self.dispose = self._crop(self.im, self.dispose_extent)
|
||||
elif frame_transparency is not None:
|
||||
|
@ -306,26 +364,30 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
dispose_size = (x1 - x0, y1 - y0)
|
||||
|
||||
Image._decompression_bomb_check(dispose_size)
|
||||
self.dispose = Image.core.fill(
|
||||
"RGBA", dispose_size, _rgb(frame_transparency) + (0,)
|
||||
)
|
||||
dispose_mode = "P"
|
||||
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:
|
||||
pass
|
||||
|
||||
if interlace is not None:
|
||||
if frame == 0 and frame_transparency is not None:
|
||||
self.info["transparency"] = frame_transparency
|
||||
transparency = -1
|
||||
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 = [
|
||||
(
|
||||
"gif",
|
||||
(x0, y0, x1, y1),
|
||||
self.__offset,
|
||||
(bits, interlace),
|
||||
(bits, interlace, transparency),
|
||||
)
|
||||
]
|
||||
else:
|
||||
# self.__fp = None
|
||||
raise EOFError
|
||||
|
||||
for k in ["duration", "comment", "extension", "loop"]:
|
||||
if k in info:
|
||||
|
@ -333,45 +395,42 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
elif k in self.info:
|
||||
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):
|
||||
temp_mode = "P" if self._frame_palette else "L"
|
||||
self._prev_im = None
|
||||
if self.__frame == 0:
|
||||
if "transparency" in self.info:
|
||||
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
|
||||
if self._frame_palette:
|
||||
self.mode = "P"
|
||||
self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
|
||||
self.im.putpalette(*self._frame_palette.getdata())
|
||||
self._frame_palette = None
|
||||
else:
|
||||
self.mode = "L"
|
||||
self.im = None
|
||||
self.mode = temp_mode
|
||||
self._frame_palette = None
|
||||
|
||||
super().load_prepare()
|
||||
|
||||
def load_end(self):
|
||||
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
|
||||
if self._frame_transparency is not None:
|
||||
self.im.putpalettealpha(self._frame_transparency, 0)
|
||||
frame_im = self.im.convert("RGBA")
|
||||
if self.mode == "P" and self._prev_im:
|
||||
if self._frame_transparency is not None:
|
||||
self.im.putpalettealpha(self._frame_transparency, 0)
|
||||
frame_im = self.im.convert("RGBA")
|
||||
else:
|
||||
frame_im = self.im.convert("RGB")
|
||||
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)
|
||||
|
||||
self.im = self._prev_im
|
||||
|
@ -401,7 +460,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
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
|
||||
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
|
||||
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 initial_call: Default false, set to true for a single frame.
|
||||
:returns: Image object
|
||||
"""
|
||||
if im.mode in RAWMODE:
|
||||
im.load()
|
||||
return im
|
||||
if Image.getmodebase(im.mode) == "RGB":
|
||||
if initial_call:
|
||||
palette_size = 256
|
||||
if im.palette:
|
||||
palette_size = len(im.palette.getdata()[1]) // 3
|
||||
im = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=palette_size)
|
||||
if im.palette.mode == "RGBA":
|
||||
for rgba in im.palette.colors.keys():
|
||||
if rgba[3] == 0:
|
||||
im.info["transparency"] = im.palette.colors[rgba]
|
||||
break
|
||||
return im
|
||||
else:
|
||||
return im.convert("P")
|
||||
im = im.convert("P", palette=Image.Palette.ADAPTIVE)
|
||||
if im.palette.mode == "RGBA":
|
||||
for rgba in im.palette.colors.keys():
|
||||
if rgba[3] == 0:
|
||||
im.info["transparency"] = im.palette.colors[rgba]
|
||||
break
|
||||
return im
|
||||
return im.convert("L")
|
||||
|
||||
|
||||
|
@ -491,7 +539,7 @@ def _normalize_palette(im, palette, info):
|
|||
|
||||
|
||||
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():
|
||||
im.encoderinfo.setdefault(k, v)
|
||||
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):
|
||||
transparent_color_exists = False
|
||||
try:
|
||||
transparency = im.encoderinfo["transparency"]
|
||||
except KeyError:
|
||||
if "transparency" in im.encoderinfo:
|
||||
transparency = im.encoderinfo["transparency"]
|
||||
else:
|
||||
transparency = im.info["transparency"]
|
||||
transparency = int(transparency)
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
else:
|
||||
transparency = int(transparency)
|
||||
# optimize the block away if transparent color is not used
|
||||
transparent_color_exists = True
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ class GimpPaletteFile:
|
|||
break
|
||||
|
||||
# skip fields and comment lines
|
||||
if re.match(br"\w+:|#", s):
|
||||
if re.match(rb"\w+:|#", s):
|
||||
continue
|
||||
if len(s) > 100:
|
||||
raise SyntaxError("bad palette file")
|
||||
|
|
|
@ -167,7 +167,7 @@ class IcnsFile:
|
|||
self.dct = dct = {}
|
||||
self.fobj = fobj
|
||||
sig, filesize = nextheader(fobj)
|
||||
if sig != MAGIC:
|
||||
if not _accept(sig):
|
||||
raise SyntaxError("not an icns file")
|
||||
i = HEADERSIZE
|
||||
while i < filesize:
|
||||
|
@ -287,7 +287,7 @@ class IcnsImageFile(ImageFile.ImageFile):
|
|||
)
|
||||
|
||||
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
|
||||
return px
|
||||
self.load_prepare()
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
|
||||
|
||||
|
||||
import struct
|
||||
import warnings
|
||||
from io import BytesIO
|
||||
from math import ceil, log
|
||||
|
@ -30,6 +29,8 @@ from math import ceil, log
|
|||
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
|
||||
from ._binary import i16le as i16
|
||||
from ._binary import i32le as i32
|
||||
from ._binary import o8
|
||||
from ._binary import o16le as o16
|
||||
from ._binary import o32le as o32
|
||||
|
||||
#
|
||||
|
@ -40,57 +41,72 @@ _MAGIC = b"\0\0\1\0"
|
|||
|
||||
def _save(im, fp, filename):
|
||||
fp.write(_MAGIC) # (2+2)
|
||||
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
|
||||
sizes = im.encoderinfo.get(
|
||||
"sizes",
|
||||
[(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
|
||||
sizes = filter(
|
||||
lambda x: False
|
||||
if (x[0] > width or x[1] > height or x[0] > 256 or x[1] > 256)
|
||||
else True,
|
||||
sizes,
|
||||
)
|
||||
sizes = list(sizes)
|
||||
fp.write(struct.pack("<H", len(sizes))) # idCount(2)
|
||||
offset = fp.tell() + len(sizes) * 16
|
||||
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
|
||||
provided_images = {im.size: im for im in im.encoderinfo.get("append_images", [])}
|
||||
for size in sizes:
|
||||
width, height = size
|
||||
for size in sorted(set(sizes)):
|
||||
if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256:
|
||||
continue
|
||||
|
||||
for provided_im in provided_ims:
|
||||
if provided_im.size != size:
|
||||
continue
|
||||
frames.append(provided_im)
|
||||
if bmp:
|
||||
bits = BmpImagePlugin.SAVE[provided_im.mode][1]
|
||||
bits_used = [bits]
|
||||
for other_im in provided_ims:
|
||||
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
|
||||
fp.write(struct.pack("B", width if width < 256 else 0)) # bWidth(1)
|
||||
fp.write(struct.pack("B", height if height < 256 else 0)) # bHeight(1)
|
||||
fp.write(b"\0") # bColorCount(1)
|
||||
fp.write(o8(width if width < 256 else 0)) # bWidth(1)
|
||||
fp.write(o8(height if height < 256 else 0)) # bHeight(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\0") # wPlanes(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)
|
||||
fp.write(o16(bits)) # wBitCount(2)
|
||||
|
||||
image_io = BytesIO()
|
||||
if bmp:
|
||||
tmp.save(image_io, "dib")
|
||||
frame.save(image_io, "dib")
|
||||
|
||||
if bits != 32:
|
||||
and_mask = Image.new("1", tmp.size)
|
||||
and_mask = Image.new("1", size)
|
||||
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:
|
||||
tmp.save(image_io, "png")
|
||||
frame.save(image_io, "png")
|
||||
image_io.seek(0)
|
||||
image_bytes = image_io.read()
|
||||
if bmp:
|
||||
image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
|
||||
bytes_len = len(image_bytes)
|
||||
fp.write(struct.pack("<I", bytes_len)) # dwBytesInRes(4)
|
||||
fp.write(struct.pack("<I", offset)) # dwImageOffset(4)
|
||||
fp.write(o32(bytes_len)) # dwBytesInRes(4)
|
||||
fp.write(o32(offset)) # dwImageOffset(4)
|
||||
current = fp.tell()
|
||||
fp.seek(offset)
|
||||
fp.write(image_bytes)
|
||||
|
@ -304,7 +320,7 @@ class IcoImageFile(ImageFile.ImageFile):
|
|||
self._size = value
|
||||
|
||||
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
|
||||
return Image.Image.load(self)
|
||||
im = self.ico.getimage(self.size)
|
||||
|
|
|
@ -100,7 +100,7 @@ for i in range(2, 33):
|
|||
# --------------------------------------------------------------------
|
||||
# Read IM directory
|
||||
|
||||
split = re.compile(br"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
|
||||
split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
|
||||
|
||||
|
||||
def number(s):
|
||||
|
|
|
@ -49,7 +49,7 @@ except ImportError:
|
|||
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
||||
# Use __version__ instead.
|
||||
from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
|
||||
from ._binary import i32le
|
||||
from ._binary import i32le, o32be, o32le
|
||||
from ._util import deferred_error, isPath
|
||||
|
||||
|
||||
|
@ -847,7 +847,7 @@ class Image:
|
|||
:returns: An image access object.
|
||||
: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
|
||||
mode, arr = self.palette.getdata()
|
||||
self.im.putpalette(mode, arr)
|
||||
|
@ -864,7 +864,7 @@ class Image:
|
|||
self.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 self.pyaccess:
|
||||
return self.pyaccess
|
||||
|
@ -975,7 +975,9 @@ class Image:
|
|||
delete_trns = False
|
||||
# transparency handling
|
||||
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
|
||||
# color to an alpha channel.
|
||||
new_im = self._new(
|
||||
|
@ -1416,6 +1418,7 @@ class Image:
|
|||
"".join(self.info["Raw profile type exif"].split("\n")[3:])
|
||||
)
|
||||
elif hasattr(self, "tag_v2"):
|
||||
self._exif.bigtiff = self.tag_v2._bigtiff
|
||||
self._exif.endian = self.tag_v2._endian
|
||||
self._exif.load_from_fp(self.fp, self.tag_v2._offset)
|
||||
if exif_info is not None:
|
||||
|
@ -1492,11 +1495,12 @@ class Image:
|
|||
|
||||
def histogram(self, mask=None, extrema=None):
|
||||
"""
|
||||
Returns a histogram for the image. The histogram is returned as
|
||||
a list of pixel counts, one for each pixel value in the source
|
||||
image. If the image has more than one band, the histograms for
|
||||
all bands are concatenated (for example, the histogram for an
|
||||
"RGB" image contains 768 values).
|
||||
Returns a histogram for the image. The histogram is returned as a
|
||||
list of pixel counts, one for each pixel value in the source
|
||||
image. Counts are grouped into 256 bins for each band, even if
|
||||
the image has more than 8 bits per band. If the image has more
|
||||
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
|
||||
by this method.
|
||||
|
@ -1564,8 +1568,8 @@ class Image:
|
|||
also use color strings as supported by the ImageColor module.
|
||||
|
||||
If a mask is given, this method updates only the regions
|
||||
indicated by the mask. You can use either "1", "L" or "RGBA"
|
||||
images (in the latter case, the alpha band is used as mask).
|
||||
indicated by the mask. You can use either "1", "L", "LA", "RGBA"
|
||||
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
|
||||
the mask is 0, the current value is preserved. Intermediate
|
||||
values will mix the two images together, including their alpha
|
||||
|
@ -1613,7 +1617,7 @@ class Image:
|
|||
elif isImageType(im):
|
||||
im.load()
|
||||
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!
|
||||
im = im.convert(self.mode)
|
||||
im = im.im
|
||||
|
@ -1716,6 +1720,8 @@ class Image:
|
|||
# FIXME: _imaging returns a confusing error message for this case
|
||||
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))
|
||||
|
||||
def putalpha(self, alpha):
|
||||
|
@ -2020,6 +2026,7 @@ class Image:
|
|||
|
||||
size = tuple(size)
|
||||
|
||||
self.load()
|
||||
if box is None:
|
||||
box = (0, 0) + self.size
|
||||
else:
|
||||
|
@ -2282,7 +2289,9 @@ class Image:
|
|||
else:
|
||||
save_handler = SAVE[format.upper()]
|
||||
|
||||
created = False
|
||||
if open_fp:
|
||||
created = not os.path.exists(filename)
|
||||
if params.get("append", False):
|
||||
# Open also for reading ("+"), because TIFF save_all
|
||||
# writer needs to go back and edit the written data.
|
||||
|
@ -2292,10 +2301,17 @@ class Image:
|
|||
|
||||
try:
|
||||
save_handler(self, fp, filename)
|
||||
finally:
|
||||
# do what we can to clean up
|
||||
except Exception:
|
||||
if open_fp:
|
||||
fp.close()
|
||||
if created:
|
||||
try:
|
||||
os.remove(filename)
|
||||
except PermissionError:
|
||||
pass
|
||||
raise
|
||||
if open_fp:
|
||||
fp.close()
|
||||
|
||||
def seek(self, frame):
|
||||
"""
|
||||
|
@ -2435,6 +2451,7 @@ class Image:
|
|||
:returns: None
|
||||
"""
|
||||
|
||||
self.load()
|
||||
x, y = map(math.floor, size)
|
||||
if x >= self.width and y >= self.height:
|
||||
return
|
||||
|
@ -2779,9 +2796,9 @@ def frombytes(mode, size, data, decoder_name="raw", *args):
|
|||
In its simplest form, this function takes three arguments
|
||||
(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
|
||||
: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.
|
||||
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):
|
||||
"""
|
||||
Creates a new image by interpolating between two input images, using
|
||||
a constant alpha.::
|
||||
a constant alpha::
|
||||
|
||||
out = image1 * (1.0 - alpha) + image2 * alpha
|
||||
|
||||
|
@ -3423,6 +3440,7 @@ atexit.register(core.clear_cache)
|
|||
|
||||
class Exif(MutableMapping):
|
||||
endian = None
|
||||
bigtiff = False
|
||||
|
||||
def __init__(self):
|
||||
self._data = {}
|
||||
|
@ -3458,10 +3476,15 @@ class Exif(MutableMapping):
|
|||
return self._fixup_dict(info)
|
||||
|
||||
def _get_head(self):
|
||||
version = b"\x2B" if self.bigtiff else b"\x2A"
|
||||
if self.endian == "<":
|
||||
return b"II\x2A\x00\x08\x00\x00\x00"
|
||||
head = b"II" + version + b"\x00" + o32le(8)
|
||||
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):
|
||||
# Extract EXIF information. This is highly experimental,
|
||||
|
@ -3475,12 +3498,12 @@ class Exif(MutableMapping):
|
|||
self._loaded_exif = data
|
||||
self._data.clear()
|
||||
self._ifds.clear()
|
||||
if data and data.startswith(b"Exif\x00\x00"):
|
||||
data = data[6:]
|
||||
if not data:
|
||||
self._info = None
|
||||
return
|
||||
|
||||
if data.startswith(b"Exif\x00\x00"):
|
||||
data = data[6:]
|
||||
self.fp = io.BytesIO(data)
|
||||
self.head = self.fp.read(8)
|
||||
# process dictionary
|
||||
|
|
|
@ -223,15 +223,15 @@ class ImageFile(Image.Image):
|
|||
)
|
||||
]
|
||||
for decoder_name, extents, offset, args in self.tile:
|
||||
seek(offset)
|
||||
decoder = Image._getdecoder(
|
||||
self.mode, decoder_name, args, self.decoderconfig
|
||||
)
|
||||
try:
|
||||
seek(offset)
|
||||
decoder.setimage(self.im, extents)
|
||||
if decoder.pulls_fd:
|
||||
decoder.setfd(self.fp)
|
||||
status, err_code = decoder.decode(b"")
|
||||
err_code = decoder.decode(b"")[1]
|
||||
else:
|
||||
b = prefix
|
||||
while True:
|
||||
|
@ -499,40 +499,33 @@ def _save(im, fp, tile, bufsize=0):
|
|||
try:
|
||||
fh = fp.fileno()
|
||||
fp.flush()
|
||||
except (AttributeError, io.UnsupportedOperation) as exc:
|
||||
# compress to Python file-compatible 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()
|
||||
exc = None
|
||||
except (AttributeError, io.UnsupportedOperation) as e:
|
||||
exc = e
|
||||
for e, b, o, a in tile:
|
||||
if o > 0:
|
||||
fp.seek(o)
|
||||
encoder = Image._getencoder(im.mode, e, a, im.encoderconfig)
|
||||
try:
|
||||
encoder.setimage(im.im, b)
|
||||
if encoder.pushes_fd:
|
||||
encoder.setfd(fp)
|
||||
l, s = encoder.encode_to_pyfd()
|
||||
else:
|
||||
while True:
|
||||
l, s, d = e.encode(bufsize)
|
||||
fp.write(d)
|
||||
if s:
|
||||
break
|
||||
if exc:
|
||||
# compress to Python file-compatible object
|
||||
while True:
|
||||
l, s, d = encoder.encode(bufsize)
|
||||
fp.write(d)
|
||||
if s:
|
||||
break
|
||||
else:
|
||||
# slight speedup: compress to real file object
|
||||
s = encoder.encode_to_file(fh, bufsize)
|
||||
if s < 0:
|
||||
raise OSError(f"encoder error {s} when writing image file") from exc
|
||||
e.cleanup()
|
||||
else:
|
||||
# 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()
|
||||
finally:
|
||||
encoder.cleanup()
|
||||
if hasattr(fp, "flush"):
|
||||
fp.flush()
|
||||
|
||||
|
@ -671,7 +664,7 @@ class PyDecoder(PyCodec):
|
|||
|
||||
:param buffer: A bytes object with the data to be decoded.
|
||||
: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`.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
@ -725,6 +718,9 @@ class PyEncoder(PyCodec):
|
|||
|
||||
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)``.
|
||||
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":
|
||||
fh, filepath = tempfile.mkstemp(".png")
|
||||
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.load()
|
||||
os.unlink(filepath)
|
||||
if bbox:
|
||||
im_cropped = im.crop(bbox)
|
||||
im_resized = im.resize((right - left, bottom - top))
|
||||
im.close()
|
||||
return im_cropped
|
||||
return im_resized
|
||||
return im
|
||||
elif sys.platform == "win32":
|
||||
offset, size, data = Image.core.grabscreen_win32(
|
||||
|
|
|
@ -525,7 +525,7 @@ def invert(image):
|
|||
lut = []
|
||||
for i in range(256):
|
||||
lut.append(255 - i)
|
||||
return _lut(image, lut)
|
||||
return image.point(lut) if image.mode == "1" else _lut(image, lut)
|
||||
|
||||
|
||||
def mirror(image):
|
||||
|
|
|
@ -32,8 +32,6 @@ class ImagePalette:
|
|||
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).
|
||||
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):
|
||||
|
|
|
@ -270,8 +270,9 @@ class DisplayViewer(UnixViewer):
|
|||
else:
|
||||
raise TypeError("Missing required argument: 'path'")
|
||||
args = ["display"]
|
||||
if "title" in options and options["title"] is not None:
|
||||
args += ["-title", options["title"]]
|
||||
title = options.get("title")
|
||||
if title:
|
||||
args += ["-title", title]
|
||||
args.append(path)
|
||||
|
||||
subprocess.Popen(args)
|
||||
|
@ -368,8 +369,9 @@ class XVViewer(UnixViewer):
|
|||
else:
|
||||
raise TypeError("Missing required argument: 'path'")
|
||||
args = ["xv"]
|
||||
if "title" in options:
|
||||
args += ["-name", options["title"]]
|
||||
title = options.get("title")
|
||||
if title:
|
||||
args += ["-name", title]
|
||||
args.append(path)
|
||||
|
||||
subprocess.Popen(args)
|
||||
|
|
|
@ -91,7 +91,7 @@ class Stat:
|
|||
for i in range(0, len(self.h), 256):
|
||||
sum2 = 0.0
|
||||
for j in range(256):
|
||||
sum2 += (j ** 2) * float(self.h[i + j])
|
||||
sum2 += (j**2) * float(self.h[i + j])
|
||||
v.append(sum2)
|
||||
return v
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#
|
||||
|
||||
import tkinter
|
||||
import warnings
|
||||
from io import BytesIO
|
||||
|
||||
from . import Image
|
||||
|
@ -58,6 +59,33 @@ def _get_image_from_kw(kw):
|
|||
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
|
||||
|
||||
|
@ -156,11 +184,15 @@ class PhotoImage:
|
|||
: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
|
||||
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
|
||||
im.load()
|
||||
image = im.im
|
||||
|
@ -170,33 +202,7 @@ class PhotoImage:
|
|||
block = image.new_block(self.__mode, im.size)
|
||||
image.convert2(block, image) # convert directly between buffers
|
||||
|
||||
tk = self.__photo.tk
|
||||
|
||||
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
|
||||
_pyimagingtkcall("PyImagingPhoto", self.__photo, block.id)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
@ -276,7 +282,7 @@ def getimage(photo):
|
|||
im = Image.new("RGBA", (photo.width(), photo.height()))
|
||||
block = im.im
|
||||
|
||||
photo.tk.call("PyImagingPhotoGet", photo, block.id)
|
||||
_pyimagingtkcall("PyImagingPhotoGet", photo, block.id)
|
||||
|
||||
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,
|
||||
to floating-point dots per inch."""
|
||||
if denom != 0:
|
||||
return (254 * num * (10 ** exp)) / (10000 * denom)
|
||||
return (254 * num * (10**exp)) / (10000 * denom)
|
||||
|
||||
|
||||
def _parse_jp2_header(fp):
|
||||
|
@ -290,14 +290,14 @@ def _accept(prefix):
|
|||
|
||||
|
||||
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"
|
||||
else:
|
||||
kind = "jp2"
|
||||
|
||||
# Get the keyword arguments
|
||||
info = im.encoderinfo
|
||||
|
||||
offset = info.get("offset", None)
|
||||
tile_offset = info.get("tile_offset", None)
|
||||
tile_size = info.get("tile_size", None)
|
||||
|
@ -320,6 +320,7 @@ def _save(im, fp, filename):
|
|||
irreversible = info.get("irreversible", False)
|
||||
progression = info.get("progression", "LRCP")
|
||||
cinema_mode = info.get("cinema_mode", "no")
|
||||
mct = info.get("mct", 0)
|
||||
fd = -1
|
||||
|
||||
if hasattr(fp, "fileno"):
|
||||
|
@ -340,6 +341,7 @@ def _save(im, fp, filename):
|
|||
irreversible,
|
||||
progression,
|
||||
cinema_mode,
|
||||
mct,
|
||||
fd,
|
||||
)
|
||||
|
||||
|
|