Merge branch 'main' into add-cygwin-to-ci

This commit is contained in:
DWesl 2022-04-15 14:47:28 -04:00 committed by GitHub
commit 7099ade15a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
129 changed files with 1632 additions and 2719 deletions

View File

@ -25,8 +25,8 @@ install:
- mv c:\pillow-depends-main c:\pillow-depends - mv c:\pillow-depends-main c:\pillow-depends
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images - xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
- ..\pillow-depends\gs9550w32.exe /S - ..\pillow-depends\gs9561w32.exe /S
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.55.0\bin;%PATH% - path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH%
- cd c:\pillow\winbuild\ - cd c:\pillow\winbuild\
- ps: | - ps: |
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
@ -43,7 +43,7 @@ build_script:
test_script: test_script:
- cd c:\pillow - cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov' - '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' - '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' - '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'

View File

@ -31,13 +31,13 @@ jobs:
language: python language: python
dry-run: false dry-run: false
- name: Upload New Crash - name: Upload New Crash
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: failure() && steps.build.outcome == 'success' if: failure() && steps.build.outcome == 'success'
with: with:
name: artifacts name: artifacts
path: ./out/artifacts path: ./out/artifacts
- name: Upload Legacy Crash - name: Upload Legacy Crash
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: steps.run.outcome == 'success' if: steps.run.outcome == 'success'
with: with:
name: crash name: crash

View File

@ -10,7 +10,7 @@ jobs:
name: Lint name: Lint
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: pre-commit cache - name: pre-commit cache
uses: actions/cache@v2 uses: actions/cache@v2
@ -21,7 +21,7 @@ jobs:
lint-pre-commit- lint-pre-commit-
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v3
with: with:
python-version: "3.10" python-version: "3.10"
cache: pip cache: pip

27
.github/workflows/stale.yml vendored Normal file
View 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"

View File

@ -23,7 +23,6 @@ jobs:
centos-stream-9-amd64, centos-stream-9-amd64,
debian-10-buster-x86, debian-10-buster-x86,
debian-11-bullseye-x86, debian-11-bullseye-x86,
fedora-34-amd64,
fedora-35-amd64, fedora-35-amd64,
gentoo, gentoo,
ubuntu-18.04-bionic-amd64, ubuntu-18.04-bionic-amd64,
@ -41,7 +40,7 @@ jobs:
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py

View File

@ -29,7 +29,7 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up shell - name: Set up shell
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH

View File

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

View File

@ -23,17 +23,17 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Checkout cached dependencies - name: Checkout cached dependencies
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
repository: python-pillow/pillow-depends repository: python-pillow/pillow-depends
path: winbuild\depends path: winbuild\depends
# sets env: pythonLocation # sets env: pythonLocation
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v3
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }} architecture: ${{ matrix.architecture }}
@ -52,8 +52,8 @@ jobs:
7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\" 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
winbuild\depends\gs9550w32.exe /S winbuild\depends\gs9561w32.exe /S
echo "C:\Program Files (x86)\gs\gs9.55.0\bin" >> $env:GITHUB_PATH echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH
xcopy /S /Y winbuild\depends\test_images\* Tests\images\ xcopy /S /Y winbuild\depends\test_images\* Tests\images\
@ -156,7 +156,7 @@ jobs:
shell: bash shell: bash
- name: Upload errors - name: Upload errors
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: failure() if: failure()
with: with:
name: errors name: errors
@ -182,7 +182,7 @@ jobs:
winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
shell: cmd shell: cmd
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v3
if: "github.event_name != 'pull_request'" if: "github.event_name != 'pull_request'"
with: with:
name: ${{ steps.wheel.outputs.dist }} name: ${{ steps.wheel.outputs.dist }}

View File

@ -36,10 +36,10 @@ jobs:
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v3
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
cache: pip cache: pip
@ -84,7 +84,7 @@ jobs:
mkdir -p Tests/errors mkdir -p Tests/errors
- name: Upload errors - name: Upload errors
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: failure() if: failure()
with: with:
name: errors name: errors
@ -93,7 +93,7 @@ jobs:
- name: Docs - name: Docs
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10 if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
run: | run: |
python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph python3 -m pip install furo sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph
make doccheck make doccheck
- name: After success - name: After success

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Scan - name: Scan
uses: tidelift/alignment-action@main uses: tidelift/alignment-action@main
env: env:

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: f1d4e742c91dd5179d742b0db9293c4472b765f8 # frozen: 21.12b0 rev: 22.3.0
hooks: hooks:
- id: black - id: black
args: ["--target-version", "py37"] args: ["--target-version", "py37"]
@ -9,35 +9,35 @@ repos:
types: [] types: []
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort
rev: c5e8fa75dda5f764d20f66a215d71c21cfa198e1 # frozen: 5.10.1 rev: 5.10.1
hooks: hooks:
- id: isort - id: isort
- repo: https://github.com/asottile/yesqa - repo: https://github.com/asottile/yesqa
rev: 35cf7dc24fa922927caded7a21b2a8cb04bf8e10 # frozen: v1.3.0 rev: v1.3.0
hooks: hooks:
- id: yesqa - id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10 rev: v1.1.13
hooks: hooks:
- id: remove-tabs - id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: cbeb4c9c4137cff1568659fcc48e8b85cddd0c8d # frozen: 4.0.1 rev: 4.0.1
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [flake8-2020, flake8-implicit-str-concat] additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0 rev: v1.9.0
hooks: hooks:
- id: python-check-blanket-noqa - id: python-check-blanket-noqa
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: 8fe62d14e0b4d7d845a7022c5c2c3ae41bdd3f26 # frozen: v4.1.0 rev: v4.1.0
hooks: hooks:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-yaml - id: check-yaml

View File

@ -2,9 +2,90 @@
Changelog (Pillow) Changelog (Pillow)
================== ==================
9.1.0 (unreleased) 9.2.0 (unreleased)
------------------ ------------------
- Round lut values where necessary #6188
[radarhere]
- Load before getting size in resize() #6190
[radarhere]
- Load image before performing size calculations in thumbnail() #6186
[radarhere]
- Deprecated PhotoImage.paste() box parameter #6178
[radarhere]
9.1.0 (2022-04-01)
------------------
- Add support for multiple component transformation to JPEG2000 #5500
[scaramallion, radarhere, hugovk]
- Fix loading FriBiDi on Alpine #6165
[nulano]
- Added setting for converting GIF P frames to RGB #6150
[radarhere]
- Allow 1 mode images to be inverted #6034
[radarhere]
- Raise ValueError when trying to save empty JPEG #6159
[radarhere]
- Always save TIFF with contiguous planar configuration #5973
[radarhere]
- Connected discontiguous polygon corners #5980
[radarhere]
- Ensure Tkinter hook is activated for getimage() #6032
[radarhere]
- Use screencapture arguments to crop on macOS #6152
[radarhere]
- Do not mark L mode JPEG as 1 bit in PDF #6151
[radarhere]
- Added support for reading I;16R TIFF images #6132
[radarhere]
- If an error occurs after creating a file, remove the file #6134
[radarhere]
- Fixed calling DisplayViewer or XVViewer without a title #6136
[radarhere]
- Retain RGBA transparency when saving multiple GIF frames #6128
[radarhere]
- Save additional ICO frames with other bit depths if supplied #6122
[radarhere]
- Handle EXIF data truncated to just the header #6124
[radarhere]
- Added support for reading BMP images with RLE8 compression #6102
[radarhere]
- Support Python distributions where _tkinter is compiled in #6006
[lukegb]
- Added support for PPM arbitrary maxval #6119
[radarhere]
- Added BigTIFF reading #6097
[radarhere]
- When converting, clip I;16 to be unsigned, not signed #6112
[radarhere]
- Fixed loading L mode GIF with transparency #6086
[radarhere]
- Improved handling of PPM header #5121 - Improved handling of PPM header #5121
[Piolie, radarhere] [Piolie, radarhere]

View File

@ -77,7 +77,7 @@ release-test:
-rm dist/*.egg -rm dist/*.egg
-rmdir dist -rmdir dist
python3 -m pytest -qq python3 -m pytest -qq
python3 -m check-manifest python3 -m check_manifest
python3 -m pyroma . python3 -m pyroma .
$(MAKE) readme $(MAKE) readme

View File

@ -24,13 +24,13 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Create and check source distribution: * [ ] Create and check source distribution:
```bash ```bash
make sdist make sdist
twine check dist/* python3 -m twine check --strict dist/*
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.: * [ ] Check and upload all binaries and source distributions e.g.:
```bash ```bash
twine check dist/* python3 -m twine check --strict dist/*
twine upload dist/Pillow-5.2.0* python3 -m twine upload dist/Pillow-5.2.0*
``` ```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` * [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py`
@ -61,13 +61,13 @@ Released as needed for security, installation or critical bug fixes.
* [ ] Create and check source distribution: * [ ] Create and check source distribution:
```bash ```bash
make sdist make sdist
twine check dist/* python3 -m twine check --strict dist/*
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.: * [ ] Check and upload all binaries and source distributions e.g.:
```bash ```bash
twine check dist/* python3 -m twine check --strict dist/*
twine upload dist/Pillow-5.2.1* python3 -m twine upload dist/Pillow-5.2.1*
``` ```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
@ -91,7 +91,7 @@ Released as needed privately to individual vendors for critical security-related
* [ ] Create and check source distribution: * [ ] Create and check source distribution:
```bash ```bash
make sdist make sdist
twine check dist/* python3 -m twine check --strict dist/*
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)

View File

@ -4,5 +4,5 @@ import sys
from PIL import Image from PIL import Image
if sys.maxsize < 2 ** 32: if sys.maxsize < 2**32:
im = Image.new("L", (999999, 999999), 0) im = Image.new("L", (999999, 999999), 0)

View File

@ -23,7 +23,7 @@ YDIM = 32769
XDIM = 48000 XDIM = 48000
pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
def _write_png(tmp_path, xdim, ydim): def _write_png(tmp_path, xdim, ydim):

View File

@ -19,7 +19,7 @@ YDIM = 32769
XDIM = 48000 XDIM = 48000
pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
def _write_png(tmp_path, xdim, ydim): def _write_png(tmp_path, xdim, ydim):

BIN
Tests/images/16bit.r.tif Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

BIN
Tests/images/no_palette.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -40,6 +40,7 @@ def test_questionable():
"rgb32fakealpha.bmp", "rgb32fakealpha.bmp",
"rgb24largepal.bmp", "rgb24largepal.bmp",
"pal8os2sp.bmp", "pal8os2sp.bmp",
"pal8rletrns.bmp",
"rgb32bf-xbgr.bmp", "rgb32bf-xbgr.bmp",
] ]
for f in get_files("q"): for f in get_files("q"):

View File

@ -110,9 +110,9 @@ class TestCoreMemory:
with pytest.raises(ValueError): with pytest.raises(ValueError):
Image.core.set_blocks_max(-1) Image.core.set_blocks_max(-1)
if sys.maxsize < 2 ** 32: if sys.maxsize < 2**32:
with pytest.raises(ValueError): with pytest.raises(ValueError):
Image.core.set_blocks_max(2 ** 29) Image.core.set_blocks_max(2**29)
@pytest.mark.skipif(is_pypy(), reason="Images not collected") @pytest.mark.skipif(is_pypy(), reason="Images not collected")
def test_set_blocks_max_stats(self): def test_set_blocks_max_stats(self):

View File

@ -4,7 +4,12 @@ import pytest
from PIL import BmpImagePlugin, Image from PIL import BmpImagePlugin, Image
from .helper import assert_image_equal, assert_image_equal_tofile, hopper from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar_tofile,
hopper,
)
def test_sanity(tmp_path): def test_sanity(tmp_path):
@ -125,6 +130,42 @@ def test_rgba_bitfields():
assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
def test_rle8():
with Image.open("Tests/images/hopper_rle8.bmp") as im:
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
# This test image has been manually hexedited
# to have rows with too much data
with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im:
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
# Signal end of bitmap before the image is finished
with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp:
data = fp.read(1063) + b"\x01"
with Image.open(io.BytesIO(data)) as im:
with pytest.raises(ValueError):
im.load()
@pytest.mark.parametrize(
"file_name,length",
(
# EOF immediately after the header
("Tests/images/hopper_rle8.bmp", 1078),
# EOF during delta
("Tests/images/bmp/q/pal8rletrns.bmp", 3670),
# EOF when reading data in absolute mode
("Tests/images/bmp/g/pal8rle.bmp", 1064),
),
)
def test_rle8_eof(file_name, length):
with open(file_name, "rb") as fp:
data = fp.read(length)
with Image.open(io.BytesIO(data)) as im:
with pytest.raises(ValueError):
im.load()
def test_offset(): def test_offset():
# This image has been hexedited # This image has been hexedited
# to exclude the palette size from the pixel data offset # to exclude the palette size from the pixel data offset

View File

@ -196,6 +196,13 @@ def test__accept_false():
assert not output assert not output
def test_invalid_file():
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
DdsImagePlugin.DdsImageFile(invalid_file)
def test_short_header(): def test_short_header():
"""Check a short header""" """Check a short header"""
with open(TEST_FILE_DXT5, "rb") as f: with open(TEST_FILE_DXT5, "rb") as f:

View File

@ -16,6 +16,13 @@ def test_load_dxt1():
assert_image_similar(im, target.convert("RGBA"), 15) assert_image_similar(im, target.convert("RGBA"), 15)
def test_invalid_file():
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
FtexImagePlugin.FtexImageFile(invalid_file)
def test_constants_deprecation(): def test_constants_deprecation():
for enum, prefix in { for enum, prefix in {
FtexImagePlugin.Format: "FORMAT_", FtexImagePlugin.Format: "FORMAT_",

View File

@ -59,6 +59,51 @@ def test_invalid_file():
GifImagePlugin.GifImageFile(invalid_file) GifImagePlugin.GifImageFile(invalid_file)
def test_l_mode_transparency():
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
assert im.mode == "L"
assert im.load()[0, 0] == 128
assert im.info["transparency"] == 255
im.seek(1)
assert im.mode == "L"
assert im.load()[0, 0] == 128
def test_strategy():
with Image.open("Tests/images/chi.gif") as im:
expected_zero = im.convert("RGB")
im.seek(1)
expected_one = im.convert("RGB")
try:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "RGB"
assert_image_equal(im, expected_zero)
GifImagePlugin.LOADING_STRATEGY = (
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
)
# Stay in P mode with only a global palette
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "P"
im.seek(1)
assert im.mode == "P"
assert_image_equal(im.convert("RGB"), expected_one)
# Change to RGB mode when a frame has an individual palette
with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "P"
im.seek(1)
assert im.mode == "RGB"
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
def test_optimize(): def test_optimize():
def test_grayscale(optimize): def test_grayscale(optimize):
im = Image.new("L", (1, 1), 0) im = Image.new("L", (1, 1), 0)
@ -383,18 +428,38 @@ def test_dispose_background_transparency():
assert px[35, 30][3] == 0 assert px[35, 30][3] == 0
def test_transparent_dispose(): @pytest.mark.parametrize(
expected_colors = [ "loading_strategy, expected_colors",
(2, 1, 2), (
((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)), (
((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)), GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
] (
with Image.open("Tests/images/transparent_dispose.gif") as img: (2, 1, 2),
for frame in range(3): ((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
img.seek(frame) ((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
for x in range(3): ),
color = img.getpixel((x, 0)) ),
assert color == expected_colors[frame][x] (
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
(
(2, 1, 2),
(0, 1, 0),
(2, 1, 2),
),
),
),
)
def test_transparent_dispose(loading_strategy, expected_colors):
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
with Image.open("Tests/images/transparent_dispose.gif") as img:
for frame in range(3):
img.seek(frame)
for x in range(3):
color = img.getpixel((x, 0))
assert color == expected_colors[frame][x]
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
def test_dispose_previous(): def test_dispose_previous():
@ -831,6 +896,17 @@ def test_rgb_transparency(tmp_path):
assert "transparency" not in reloaded.info assert "transparency" not in reloaded.info
def test_rgba_transparency(tmp_path):
out = str(tmp_path / "temp.gif")
im = hopper("P")
im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])
with Image.open(out) as reloaded:
reloaded.seek(1)
assert_image_equal(hopper("P").convert("RGB"), reloaded)
def test_bbox(tmp_path): def test_bbox(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
@ -960,6 +1036,11 @@ def test_lzw_bits():
def test_extents(): def test_extents():
with Image.open("Tests/images/test_extents.gif") as im: with Image.open("Tests/images/test_extents.gif") as im:
assert im.size == (100, 100) assert im.size == (100, 100)
# Check that n_frames does not change the size
assert im.n_frames == 2
assert im.size == (100, 100)
im.seek(1) im.seek(1)
assert im.size == (150, 150) assert im.size == (150, 150)

View File

@ -1,4 +1,5 @@
import io import io
import os
import pytest import pytest
@ -70,6 +71,53 @@ def test_save_to_bytes():
) )
def test_no_duplicates(tmp_path):
temp_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.ico")
im = hopper()
sizes = [(32, 32), (64, 64)]
im.save(temp_file, "ico", sizes=sizes)
sizes.append(sizes[-1])
im.save(temp_file2, "ico", sizes=sizes)
assert os.path.getsize(temp_file) == os.path.getsize(temp_file2)
def test_different_bit_depths(tmp_path):
temp_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.ico")
im = hopper()
im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)])
hopper("1").save(
temp_file2,
"ico",
bitmap_format="bmp",
sizes=[(128, 128)],
append_images=[im],
)
assert os.path.getsize(temp_file) != os.path.getsize(temp_file2)
# Test that only matching sizes of different bit depths are saved
temp_file3 = str(tmp_path / "temp3.ico")
temp_file4 = str(tmp_path / "temp4.ico")
im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)])
im.save(
temp_file4,
"ico",
bitmap_format="bmp",
sizes=[(128, 128)],
append_images=[Image.new("P", (64, 64))],
)
assert os.path.getsize(temp_file3) == os.path.getsize(temp_file4)
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
def test_save_to_bytes_bmp(mode): def test_save_to_bytes_bmp(mode):
output = io.BytesIO() output = io.BytesIO()

View File

@ -68,6 +68,13 @@ class TestFileJpeg:
assert im.format == "JPEG" assert im.format == "JPEG"
assert im.get_format_mimetype() == "image/jpeg" assert im.get_format_mimetype() == "image/jpeg"
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero(self, size, tmp_path):
f = str(tmp_path / "temp.jpg")
im = Image.new("RGB", size)
with pytest.raises(ValueError):
im.save(f)
def test_app(self): def test_app(self):
# Test APP/COM reader (@PIL135) # Test APP/COM reader (@PIL135)
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:

View File

@ -209,6 +209,49 @@ def test_layers():
assert_image_similar(im, test_card, 0.4) assert_image_similar(im, test_card, 0.4)
@pytest.mark.parametrize(
"name, args, offset, data",
(
("foo.j2k", {}, 0, b"\xff\x4f"),
("foo.jp2", {}, 4, b"jP"),
(None, {"no_jp2": True}, 0, b"\xff\x4f"),
("foo.j2k", {"no_jp2": True}, 0, b"\xff\x4f"),
("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"),
("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"),
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
),
)
def test_no_jp2(name, args, offset, data):
out = BytesIO()
if name:
out.name = name
test_card.save(out, "JPEG2000", **args)
out.seek(offset)
assert out.read(2) == data
def test_mct():
# Three component
for val in (0, 1):
out = BytesIO()
test_card.save(out, "JPEG2000", mct=val, no_jp2=True)
assert out.getvalue()[59] == val
with Image.open(out) as im:
assert_image_similar(im, test_card, 1.0e-3)
# Single component should have MCT disabled
for val in (0, 1):
out = BytesIO()
with Image.open("Tests/images/16bit.cropped.jp2") as jp2:
jp2.save(out, "JPEG2000", mct=val, no_jp2=True)
assert out.getvalue()[53] == 0
with Image.open(out) as im:
assert_image_similar(im, jp2, 1.0e-3)
def test_rgba(): def test_rgba():
# Arrange # Arrange
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:

View File

@ -4,7 +4,6 @@ import itertools
import os import os
import re import re
from collections import namedtuple from collections import namedtuple
from ctypes import c_float
import pytest import pytest
@ -168,14 +167,11 @@ class TestFileLibTiff(LibTiffTestCase):
val = original[tag] val = original[tag]
if tag.endswith("Resolution"): if tag.endswith("Resolution"):
if legacy_api: if legacy_api:
assert ( assert val[0][0] / val[0][1] == (
c_float(val[0][0] / val[0][1]).value 4294967295 / 113653537
== c_float(value[0][0] / value[0][1]).value
), f"{tag} didn't roundtrip" ), f"{tag} didn't roundtrip"
else: else:
assert ( assert val == 37.79000115940079, f"{tag} didn't roundtrip"
c_float(val).value == c_float(value).value
), f"{tag} didn't roundtrip"
else: else:
assert val == value, f"{tag} didn't roundtrip" assert val == value, f"{tag} didn't roundtrip"
@ -218,7 +214,7 @@ class TestFileLibTiff(LibTiffTestCase):
values = { values = {
2: "test", 2: "test",
3: 1, 3: 1,
4: 2 ** 20, 4: 2**20,
5: TiffImagePlugin.IFDRational(100, 1), 5: TiffImagePlugin.IFDRational(100, 1),
12: 1.05, 12: 1.05,
} }
@ -1019,7 +1015,7 @@ class TestFileLibTiff(LibTiffTestCase):
im = hopper("RGB").resize((256, 256)) im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
TiffImagePlugin.STRIP_SIZE = 2 ** 18 TiffImagePlugin.STRIP_SIZE = 2**18
try: try:
im.save(out, compression="tiff_adobe_deflate") im.save(out, compression="tiff_adobe_deflate")

View File

@ -13,16 +13,53 @@ TEST_FILE = "Tests/images/hopper.ppm"
def test_sanity(): def test_sanity():
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load()
assert im.mode == "RGB" assert im.mode == "RGB"
assert im.size == (128, 128) assert im.size == (128, 128)
assert im.format, "PPM" assert im.format == "PPM"
assert im.get_format_mimetype() == "image/x-portable-pixmap" assert im.get_format_mimetype() == "image/x-portable-pixmap"
@pytest.mark.parametrize(
"data, mode, pixels",
(
(b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
# P6 with maxval < 255
(
b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11",
"RGB",
(
(0, 15, 30),
(120, 135, 150),
(225, 240, 255),
),
),
# P6 with maxval > 255
# Scale down to 255, since there is no RGB mode with more than 8-bit
(
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
"RGB",
(
(0, 1, 2),
(127, 128, 129),
(254, 255, 255),
),
),
),
)
def test_arbitrary_maxval(data, mode, pixels):
fp = BytesIO(data)
with Image.open(fp) as im:
assert im.size == (3, 1)
assert im.mode == mode
px = im.load()
assert tuple(px[x, 0] for x in range(3)) == pixels
def test_16bit_pgm(): def test_16bit_pgm():
with Image.open("Tests/images/16_bit_binary.pgm") as im: with Image.open("Tests/images/16_bit_binary.pgm") as im:
im.load()
assert im.mode == "I" assert im.mode == "I"
assert im.size == (20, 100) assert im.size == (20, 100)
assert im.get_format_mimetype() == "image/x-portable-graymap" assert im.get_format_mimetype() == "image/x-portable-graymap"
@ -32,8 +69,6 @@ def test_16bit_pgm():
def test_16bit_pgm_write(tmp_path): def test_16bit_pgm_write(tmp_path):
with Image.open("Tests/images/16_bit_binary.pgm") as im: with Image.open("Tests/images/16_bit_binary.pgm") as im:
im.load()
f = str(tmp_path / "temp.pgm") f = str(tmp_path / "temp.pgm")
im.save(f, "PPM") im.save(f, "PPM")
@ -91,19 +126,8 @@ def test_token_too_long(tmp_path):
assert str(e.value) == "Token too long in file header: b'01234567890'" assert str(e.value) == "Token too long in file header: b'01234567890'"
def test_too_many_colors(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\n1 1\n1000\n")
with pytest.raises(ValueError) as e:
with Image.open(path):
pass
assert str(e.value) == "Too many colors for band: 1000"
def test_truncated_file(tmp_path): def test_truncated_file(tmp_path):
# Test EOF in header
path = str(tmp_path / "temp.pgm") path = str(tmp_path / "temp.pgm")
with open(path, "w") as f: with open(path, "w") as f:
f.write("P6") f.write("P6")
@ -114,6 +138,12 @@ def test_truncated_file(tmp_path):
assert str(e.value) == "Reached EOF while reading header" assert str(e.value) == "Reached EOF while reading header"
# Test EOF for PyDecoder
fp = BytesIO(b"P5 3 1 4")
with Image.open(fp) as im:
with pytest.raises(ValueError):
im.load()
def test_neg_ppm(): def test_neg_ppm():
# Storage.c accepted negative values for xsize, ysize. the # Storage.c accepted negative values for xsize, ysize. the

View File

@ -87,6 +87,10 @@ class TestFileTiff:
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
def test_bigtiff(self):
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
@pytest.mark.parametrize( @pytest.mark.parametrize(
"file_name,mode,size,offset", "file_name,mode,size,offset",
[ [
@ -221,6 +225,15 @@ class TestFileTiff:
assert b[0] == ord(b"\x01") assert b[0] == ord(b"\x01")
assert b[1] == ord(b"\xe0") assert b[1] == ord(b"\xe0")
def test_16bit_r(self):
with Image.open("Tests/images/16bit.r.tif") as im:
assert im.getpixel((0, 0)) == 480
assert im.mode == "I;16"
b = im.tobytes()
assert b[0] == ord(b"\xe0")
assert b[1] == ord(b"\x01")
def test_16bit_s(self): def test_16bit_s(self):
with Image.open("Tests/images/16bit.s.tif") as im: with Image.open("Tests/images/16bit.s.tif") as im:
im.load() im.load()
@ -598,6 +611,17 @@ class TestFileTiff:
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
def test_planar_configuration_save(self, tmp_path):
infile = "Tests/images/tiff_tiled_planar_raw.tif"
with Image.open(infile) as im:
assert im._planar_configuration == 2
outfile = str(tmp_path / "temp.tif")
im.save(outfile)
with Image.open(outfile) as reloaded:
assert_image_equal_tofile(reloaded, infile)
def test_palette(self, tmp_path): def test_palette(self, tmp_path):
def roundtrip(mode): def roundtrip(mode):
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")

View File

@ -258,7 +258,7 @@ def test_ifd_unsigned_rational(tmp_path):
im = hopper() im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2() info = TiffImagePlugin.ImageFileDirectory_v2()
max_long = 2 ** 32 - 1 max_long = 2**32 - 1
# 4 bytes unsigned long # 4 bytes unsigned long
numerator = max_long numerator = max_long
@ -290,8 +290,8 @@ def test_ifd_signed_rational(tmp_path):
info = TiffImagePlugin.ImageFileDirectory_v2() info = TiffImagePlugin.ImageFileDirectory_v2()
# pair of 4 byte signed longs # pair of 4 byte signed longs
numerator = 2 ** 31 - 1 numerator = 2**31 - 1
denominator = -(2 ** 31) denominator = -(2**31)
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
@ -302,8 +302,8 @@ def test_ifd_signed_rational(tmp_path):
assert numerator == reloaded.tag_v2[37380].numerator assert numerator == reloaded.tag_v2[37380].numerator
assert denominator == reloaded.tag_v2[37380].denominator assert denominator == reloaded.tag_v2[37380].denominator
numerator = -(2 ** 31) numerator = -(2**31)
denominator = 2 ** 31 - 1 denominator = 2**31 - 1
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
@ -315,7 +315,7 @@ def test_ifd_signed_rational(tmp_path):
assert denominator == reloaded.tag_v2[37380].denominator assert denominator == reloaded.tag_v2[37380].denominator
# out of bounds of 4 byte signed long # out of bounds of 4 byte signed long
numerator = -(2 ** 31) - 1 numerator = -(2**31) - 1
denominator = 1 denominator = 1
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
@ -324,7 +324,7 @@ def test_ifd_signed_rational(tmp_path):
im.save(out, tiffinfo=info, compression="raw") im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert 2 ** 31 - 1 == reloaded.tag_v2[37380].numerator assert 2**31 - 1 == reloaded.tag_v2[37380].numerator
assert -1 == reloaded.tag_v2[37380].denominator assert -1 == reloaded.tag_v2[37380].denominator

View File

@ -8,6 +8,7 @@ import pytest
from PIL import Image, WebPImagePlugin, features from PIL import Image, WebPImagePlugin, features
from .helper import ( from .helper import (
assert_image_equal,
assert_image_similar, assert_image_similar,
assert_image_similar_tofile, assert_image_similar_tofile,
hopper, hopper,
@ -105,6 +106,19 @@ class TestFileWebp:
hopper().save(buffer_method, format="WEBP", method=6) hopper().save(buffer_method, format="WEBP", method=6)
assert buffer_no_args.getbuffer() != buffer_method.getbuffer() assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
@skip_unless_feature("webp_anim")
def test_save_all(self, tmp_path):
temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00")
im.save(temp_file, save_all=True, append_images=[im2])
with Image.open(temp_file) as reloaded:
assert_image_equal(im, reloaded)
reloaded.seek(1)
assert_image_similar(im2, reloaded, 1)
def test_icc_profile(self, tmp_path): def test_icc_profile(self, tmp_path):
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
if _webp.HAVE_WEBPANIM: if _webp.HAVE_WEBPANIM:
@ -128,7 +142,7 @@ class TestFileWebp:
self._roundtrip(tmp_path, "P", 50.0) self._roundtrip(tmp_path, "P", 50.0)
@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_message(self, tmp_path): def test_write_encoding_error_message(self, tmp_path):
temp_file = str(tmp_path / "temp.webp") temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (15000, 15000)) im = Image.new("RGB", (15000, 15000))
@ -171,9 +185,14 @@ class TestFileWebp:
Image.open(blob).load() Image.open(blob).load()
Image.open(blob).load() Image.open(blob).load()
@skip_unless_feature("webp")
@skip_unless_feature("webp_anim") @skip_unless_feature("webp_anim")
def test_background_from_gif(self, tmp_path): def test_background_from_gif(self, tmp_path):
# Save L mode GIF with background
with Image.open("Tests/images/no_palette_with_background.gif") as im:
out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)
# Save P mode GIF with background
with Image.open("Tests/images/chi.gif") as im: with Image.open("Tests/images/chi.gif") as im:
original_value = im.convert("RGB").getpixel((1, 1)) original_value = im.convert("RGB").getpixel((1, 1))
@ -191,7 +210,6 @@ class TestFileWebp:
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
assert difference < 5 assert difference < 5
@skip_unless_feature("webp")
@skip_unless_feature("webp_anim") @skip_unless_feature("webp_anim")
def test_duration(self, tmp_path): def test_duration(self, tmp_path):
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:

View File

@ -1,6 +1,7 @@
import pytest import pytest
from packaging.version import parse as parse_version
from PIL import Image from PIL import Image, features
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -27,7 +28,6 @@ def test_n_frames():
assert im.is_animated assert im.is_animated
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_write_animation_L(tmp_path): def test_write_animation_L(tmp_path):
""" """
Convert an animated GIF to animated WebP, then compare the frame count, and first Convert an animated GIF to animated WebP, then compare the frame count, and first
@ -46,6 +46,11 @@ def test_write_animation_L(tmp_path):
orig.load() orig.load()
im.load() im.load()
assert_image_similar(im, orig.convert("RGBA"), 32.9) assert_image_similar(im, orig.convert("RGBA"), 32.9)
if is_big_endian():
webp = parse_version(features.version_module("webp"))
if webp < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
orig.seek(orig.n_frames - 1) orig.seek(orig.n_frames - 1)
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
orig.load() orig.load()
@ -53,7 +58,6 @@ def test_write_animation_L(tmp_path):
assert_image_similar(im, orig.convert("RGBA"), 32.9) assert_image_similar(im, orig.convert("RGBA"), 32.9)
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_write_animation_RGB(tmp_path): def test_write_animation_RGB(tmp_path):
""" """
Write an animated WebP from RGB frames, and ensure the frames Write an animated WebP from RGB frames, and ensure the frames
@ -69,6 +73,10 @@ def test_write_animation_RGB(tmp_path):
assert_image_equal(im, frame1.convert("RGBA")) assert_image_equal(im, frame1.convert("RGBA"))
# Compare second frame to original # Compare second frame to original
if is_big_endian():
webp = parse_version(features.version_module("webp"))
if webp < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
im.seek(1) im.seek(1)
im.load() im.load()
assert_image_equal(im, frame2.convert("RGBA")) assert_image_equal(im, frame2.convert("RGBA"))

View File

@ -2,7 +2,7 @@ from io import BytesIO
import pytest import pytest
from PIL import Image from PIL import Image, XbmImagePlugin
from .helper import hopper from .helper import hopper
@ -63,6 +63,13 @@ def test_open_filename_with_underscore():
assert im.size == (128, 128) assert im.size == (128, 128)
def test_invalid_file():
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
XbmImagePlugin.XbmImageFile(invalid_file)
def test_save_wrong_mode(tmp_path): def test_save_wrong_mode(tmp_path):
im = hopper() im = hopper()
out = str(tmp_path / "temp.xbm") out = str(tmp_path / "temp.xbm")

View File

@ -652,6 +652,15 @@ class TestImage:
with warnings.catch_warnings(): with warnings.catch_warnings():
im.save(temp_file) im.save(temp_file)
def test_no_new_file_on_error(self, tmp_path):
temp_file = str(tmp_path / "temp.jpg")
im = Image.new("RGB", (0, 0))
with pytest.raises(ValueError):
im.save(temp_file)
assert not os.path.exists(temp_file)
def test_load_on_nonexclusive_multiframe(self): def test_load_on_nonexclusive_multiframe(self):
with open("Tests/images/frozenpond.mpo", "rb") as fp: with open("Tests/images/frozenpond.mpo", "rb") as fp:
@ -666,6 +675,19 @@ class TestImage:
assert not fp.closed assert not fp.closed
def test_empty_exif(self):
with Image.open("Tests/images/exif.png") as im:
exif = im.getexif()
assert dict(exif) != {}
# Test that exif data is cleared after another load
exif.load(None)
assert dict(exif) == {}
# Test loading just the EXIF header
exif.load(b"Exif\x00\x00")
assert dict(exif) == {}
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )

View File

@ -1,4 +1,3 @@
import ctypes
import os import os
import subprocess import subprocess
import sys import sys
@ -154,14 +153,17 @@ class TestImageGetPixel(AccessTest):
# Check 0 # Check 0
im = Image.new(mode, (0, 0), None) im = Image.new(mode, (0, 0), None)
with pytest.raises(IndexError): assert im.load() is not None
error = ValueError if self._need_cffi_access else IndexError
with pytest.raises(error):
im.putpixel((0, 0), c) im.putpixel((0, 0), c)
with pytest.raises(IndexError): with pytest.raises(error):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check 0 negative index # Check 0 negative index
with pytest.raises(IndexError): with pytest.raises(error):
im.putpixel((-1, -1), c) im.putpixel((-1, -1), c)
with pytest.raises(IndexError): with pytest.raises(error):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
# check initial color # check initial color
@ -176,10 +178,10 @@ class TestImageGetPixel(AccessTest):
# Check 0 # Check 0
im = Image.new(mode, (0, 0), c) im = Image.new(mode, (0, 0), c)
with pytest.raises(IndexError): with pytest.raises(error):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check 0 negative index # Check 0 negative index
with pytest.raises(IndexError): with pytest.raises(error):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
def test_basic(self): def test_basic(self):
@ -205,10 +207,10 @@ class TestImageGetPixel(AccessTest):
# see https://github.com/python-pillow/Pillow/issues/452 # see https://github.com/python-pillow/Pillow/issues/452
# pixelaccess is using signed int* instead of uint* # pixelaccess is using signed int* instead of uint*
for mode in ("I;16", "I;16B"): for mode in ("I;16", "I;16B"):
self.check(mode, 2 ** 15 - 1) self.check(mode, 2**15 - 1)
self.check(mode, 2 ** 15) self.check(mode, 2**15)
self.check(mode, 2 ** 15 + 1) self.check(mode, 2**15 + 1)
self.check(mode, 2 ** 16 - 1) self.check(mode, 2**16 - 1)
def test_p_putpixel_rgb_rgba(self): def test_p_putpixel_rgb_rgba(self):
for color in [(255, 0, 0), (255, 0, 0, 255)]: for color in [(255, 0, 0), (255, 0, 0, 255)]:
@ -386,7 +388,7 @@ class TestImagePutPixelError(AccessTest):
def test_putpixel_overflow_error(self, mode): def test_putpixel_overflow_error(self, mode):
im = hopper(mode) im = hopper(mode)
with pytest.raises(OverflowError): with pytest.raises(OverflowError):
im.putpixel((0, 0), 2 ** 80) im.putpixel((0, 0), 2**80)
def test_putpixel_unrecognized_mode(self): def test_putpixel_unrecognized_mode(self):
im = hopper("BGR;15") im = hopper("BGR;15")
@ -401,6 +403,8 @@ class TestEmbeddable:
"not from shell", "not from shell",
) )
def test_embeddable(self): def test_embeddable(self):
import ctypes
with open("embed_pil.c", "w") as fh: with open("embed_pil.c", "w") as fh:
fh.write( fh.write(
""" """

View File

@ -70,6 +70,11 @@ def test_16bit():
with Image.open("Tests/images/16bit.cropped.tif") as im: with Image.open("Tests/images/16bit.cropped.tif") as im:
_test_float_conversion(im) _test_float_conversion(im)
for color in (65535, 65536):
im = Image.new("I", (1, 1), color)
im_i16 = im.convert("I;16")
assert im_i16.getpixel((0, 0)) == 65535
def test_16bit_workaround(): def test_16bit_workaround():
with Image.open("Tests/images/16bit.cropped.tif") as im: with Image.open("Tests/images/16bit.cropped.tif") as im:
@ -135,6 +140,10 @@ def test_trns_l(tmp_path):
f = str(tmp_path / "temp.png") f = str(tmp_path / "temp.png")
im_la = im.convert("LA")
assert "transparency" not in im_la.info
im_la.save(f)
im_rgb = im.convert("RGB") im_rgb = im.convert("RGB")
assert im_rgb.info["transparency"] == (128, 128, 128) # undone assert im_rgb.info["transparency"] == (128, 128, 128) # undone
im_rgb.save(f) im_rgb.save(f)

View File

@ -67,6 +67,16 @@ class TestImagingPaste:
], ],
) )
@cached_property
def gradient_LA(self):
return Image.merge(
"LA",
[
self.gradient_L,
self.gradient_L.transpose(Image.Transpose.ROTATE_90),
],
)
@cached_property @cached_property
def gradient_RGBA(self): def gradient_RGBA(self):
return Image.merge( return Image.merge(
@ -145,6 +155,28 @@ class TestImagingPaste:
], ],
) )
def test_image_mask_LA(self):
for mode in ("RGBA", "RGB", "L"):
im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode)
self.assert_9points_paste(
im,
im2,
self.gradient_LA,
[
(128, 191, 255, 191),
(112, 207, 206, 111),
(128, 254, 128, 1),
(208, 208, 239, 239),
(192, 191, 191, 191),
(207, 207, 112, 113),
(255, 255, 255, 255),
(239, 207, 207, 239),
(255, 191, 128, 191),
],
)
def test_image_mask_RGBA(self): def test_image_mask_RGBA(self):
for mode in ("RGBA", "RGB", "L"): for mode in ("RGBA", "RGB", "L"):
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")

View File

@ -10,6 +10,7 @@ def test_sanity():
im.point(list(range(256))) im.point(list(range(256)))
im.point(list(range(256)) * 3) im.point(list(range(256)) * 3)
im.point(lambda x: x) im.point(lambda x: x)
im.point(lambda x: x * 1.2)
im = im.convert("I") im = im.convert("I")
with pytest.raises(ValueError): with pytest.raises(ValueError):

View File

@ -38,7 +38,7 @@ def test_long_integers():
assert put(0xFFFFFFFF) == (255, 255, 255, 255) assert put(0xFFFFFFFF) == (255, 255, 255, 255)
assert put(-1) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255)
assert put(-1) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255)
if sys.maxsize > 2 ** 32: if sys.maxsize > 2**32:
assert put(sys.maxsize) == (255, 255, 255, 255) assert put(sys.maxsize) == (255, 255, 255, 255)
else: else:
assert put(sys.maxsize) == (255, 255, 255, 127) assert put(sys.maxsize) == (255, 255, 255, 127)

View File

@ -264,6 +264,13 @@ class TestImageResize:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.resize((10, 10), "unknown") im.resize((10, 10), "unknown")
def test_load_first(self):
# load() may change the size of the image
# Test that resize() is calling it before getting the size
with Image.open("Tests/images/g4_orientation_5.tif") as im:
im = im.resize((64, 64))
assert im.size == (64, 64)
def test_default_filter(self): def test_default_filter(self):
for mode in "L", "RGB", "I", "F": for mode in "L", "RGB", "I", "F":
im = hopper(mode) im = hopper(mode)

View File

@ -88,6 +88,14 @@ def test_no_resize():
assert im.size == (64, 64) assert im.size == (64, 64)
def test_load_first():
# load() may change the size of the image
# Test that thumbnail() is calling it before performing size calculations
with Image.open("Tests/images/g4_orientation_5.tif") as im:
im.thumbnail((64, 64))
assert im.size == (64, 10)
# valgrind test is failing with memory allocated in libjpeg # valgrind test is failing with memory allocated in libjpeg
@pytest.mark.valgrind_known_error(reason="Known Failing") @pytest.mark.valgrind_known_error(reason="Known Failing")
def test_DCT_scaling_edges(): def test_DCT_scaling_edges():
@ -130,4 +138,4 @@ def test_reducing_gap_for_DCT_scaling():
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0) im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
assert_image_equal(ref, im) assert_image_similar(ref, im, 1.4)

View File

@ -303,7 +303,7 @@ def test_extended_information():
def assert_truncated_tuple_equal(tup1, tup2, digits=10): def assert_truncated_tuple_equal(tup1, tup2, digits=10):
# Helper function to reduce precision of tuples of floats # Helper function to reduce precision of tuples of floats
# recursively and then check equality. # recursively and then check equality.
power = 10 ** digits power = 10**digits
def truncate_tuple(tuple_or_float): def truncate_tuple(tuple_or_float):
return tuple( return tuple(

View File

@ -1440,3 +1440,15 @@ def test_continuous_horizontal_edges_polygon():
assert_image_equal_tofile( assert_image_equal_tofile(
img, expected, "continuous horizontal edges polygon failed" img, expected, "continuous horizontal edges polygon failed"
) )
def test_discontiguous_corners_polygon():
img, draw = create_base_image_draw((84, 68))
draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK)
draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK)
draw.polygon(
((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)),
BLACK,
)
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
assert_image_similar_tofile(img, expected, 1)

View File

@ -200,6 +200,9 @@ class MockPyEncoder(ImageFile.PyEncoder):
def encode(self, buffer): def encode(self, buffer):
return 1, 1, b"" return 1, 1, b""
def cleanup(self):
self.cleanup_called = True
xoff, yoff, xsize, ysize = 10, 20, 100, 100 xoff, yoff, xsize, ysize = 10, 20, 100, 100
@ -327,10 +330,12 @@ class TestPyEncoder(CodecsTest):
im = MockImageFile(buf) im = MockImageFile(buf)
fp = BytesIO() fp = BytesIO()
self.encoder.cleanup_called = False
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
) )
assert self.encoder.cleanup_called
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(

View File

@ -48,10 +48,6 @@ def img_string_normalize(im):
return img_to_string(string_to_img(im)) return img_to_string(string_to_img(im))
def assert_img_equal(A, B):
assert img_to_string(A) == img_to_string(B)
def assert_img_equal_img_string(A, Bstring): def assert_img_equal_img_string(A, Bstring):
assert img_to_string(A) == img_string_normalize(Bstring) assert img_to_string(A) == img_string_normalize(Bstring)

View File

@ -63,6 +63,7 @@ def test_sanity():
ImageOps.grayscale(hopper("L")) ImageOps.grayscale(hopper("L"))
ImageOps.grayscale(hopper("RGB")) ImageOps.grayscale(hopper("RGB"))
ImageOps.invert(hopper("1"))
ImageOps.invert(hopper("L")) ImageOps.invert(hopper("L"))
ImageOps.invert(hopper("RGB")) ImageOps.invert(hopper("RGB"))

View File

@ -32,10 +32,10 @@ def test_rgb():
def checkrgb(r, g, b): def checkrgb(r, g, b):
val = ImageQt.rgb(r, g, b) val = ImageQt.rgb(r, g, b)
val = val % 2 ** 24 # drop the alpha val = val % 2**24 # drop the alpha
assert val >> 16 == r assert val >> 16 == r
assert ((val >> 8) % 2 ** 8) == g assert ((val >> 8) % 2**8) == g
assert val % 2 ** 8 == b assert val % 2**8 == b
checkrgb(0, 0, 0) checkrgb(0, 0, 0)
checkrgb(255, 0, 0) checkrgb(255, 0, 0)

View File

@ -51,8 +51,8 @@ def test_constant():
st = ImageStat.Stat(im) st = ImageStat.Stat(im)
assert st.extrema[0] == (128, 128) assert st.extrema[0] == (128, 128)
assert st.sum[0] == 128 ** 3 assert st.sum[0] == 128**3
assert st.sum2[0] == 128 ** 4 assert st.sum2[0] == 128**4
assert st.mean[0] == 128 assert st.mean[0] == 128
assert st.median[0] == 128 assert st.median[0] == 128
assert st.rms[0] == 128 assert st.rms[0] == 128

View File

@ -75,8 +75,16 @@ def test_photoimage_blank():
assert im_tk.width() == 100 assert im_tk.width() == 100
assert im_tk.height() == 100 assert im_tk.height() == 100
# reloaded = ImageTk.getimage(im_tk) im = Image.new(mode, (100, 100))
# assert_image_equal(reloaded, im) reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded.convert(mode), im)
def test_box_deprecation():
im = hopper()
im_tk = ImageTk.PhotoImage(im)
with pytest.warns(DeprecationWarning):
im_tk.paste(im, (0, 0, 128, 128))
def test_bitmapimage(): def test_bitmapimage():

View File

@ -1,4 +1,3 @@
import ctypes
from io import BytesIO from io import BytesIO
from PIL import Image, ImageWin from PIL import Image, ImageWin
@ -8,6 +7,7 @@ from .helper import hopper, is_win32
# see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652 # see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652
if is_win32(): if is_win32():
import ctypes
import ctypes.wintypes import ctypes.wintypes
class BITMAPFILEHEADER(ctypes.Structure): class BITMAPFILEHEADER(ctypes.Structure):

View File

@ -444,6 +444,8 @@ class TestLibUnpack:
self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0)) self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0))
self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16))
self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15))
self.assert_unpack("RGBA", "BGRA;16L", 8, (6, 4, 2, 8), (14, 12, 10, 16))
self.assert_unpack("RGBA", "BGRA;16B", 8, (5, 3, 1, 7), (13, 11, 9, 15))
self.assert_unpack( self.assert_unpack(
"RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12) "RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)
) )

View File

@ -36,7 +36,7 @@ def test_tobytes():
Image.MAX_IMAGE_PIXELS = max_pixels Image.MAX_IMAGE_PIXELS = max_pixels
@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_ysize(): def test_ysize():
numpy = pytest.importorskip("numpy", reason="NumPy not installed") numpy = pytest.importorskip("numpy", reason="NumPy not installed")

View File

@ -115,6 +115,6 @@ def test_pdf_repr():
assert pdf_repr(True) == b"true" assert pdf_repr(True) == b"true"
assert pdf_repr(False) == b"false" assert pdf_repr(False) == b"false"
assert pdf_repr(None) == b"null" assert pdf_repr(None) == b"null"
assert pdf_repr(b"a)/b\\(c") == br"(a\)/b\\\(c)" assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)"
assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]"
assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>"

View File

@ -16,8 +16,6 @@
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.')) # sys.path.insert(0, os.path.abspath('.'))
import sphinx_rtd_theme
import PIL import PIL
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
@ -126,13 +124,15 @@ nitpicky = True
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
html_theme = "sphinx_rtd_theme" html_theme = "furo"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
# documentation. # documentation.
# html_theme_options = {} html_theme_options = {
"light_logo": "pillow-logo-dark-text.png",
"dark_logo": "pillow-logo.png",
}
# Add any paths that contain custom themes here, relative to this directory. # Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = [] # html_theme_path = []
@ -146,7 +146,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# The name of an image file (relative to this directory) to place at the top # The name of an image file (relative to this directory) to place at the top
# of the sidebar. # of the sidebar.
html_logo = "resources/pillow-logo.png" # html_logo = "resources/pillow-logo.png"
# The name of an image file (within the static path) to use as favicon of the # The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
@ -311,10 +311,7 @@ texinfo_documents = [
def setup(app): def setup(app):
app.add_js_file("js/script.js")
app.add_css_file("css/styles.css")
app.add_css_file("css/dark.css") app.add_css_file("css/dark.css")
app.add_css_file("css/light.css")
# GitHub repo for sphinx-issues # GitHub repo for sphinx-issues

View File

@ -69,7 +69,7 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
Constants Constants
~~~~~~~~~ ~~~~~~~~~
.. deprecated:: 9.2.0 .. deprecated:: 9.1.0
A number of constants have been deprecated and will be removed in Pillow 10.0.0 A number of constants have been deprecated and will be removed in Pillow 10.0.0
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. (2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
@ -142,6 +142,13 @@ The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be re
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
:mod:`~PIL.FitsImagePlugin` instead. :mod:`~PIL.FitsImagePlugin` instead.
PhotoImage.paste box parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 9.2.0
The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01).
Removed features Removed features
---------------- ----------------

View File

@ -210,7 +210,9 @@ class DdsImageFile(ImageFile.ImageFile):
format_description = "DirectDraw Surface" format_description = "DirectDraw Surface"
def _open(self): def _open(self):
magic, header_size = struct.unpack("<II", self.fp.read(8)) if not _accept(self.fp.read(4)):
raise SyntaxError("not a DDS file")
(header_size,) = struct.unpack("<I", self.fp.read(4))
if header_size != 124: if header_size != 124:
raise OSError(f"Unsupported header size {repr(header_size)}") raise OSError(f"Unsupported header size {repr(header_size)}")
header_bytes = self.fp.read(header_size - 4) header_bytes = self.fp.read(header_size - 4)
@ -251,7 +253,7 @@ class DXT1Decoder(ImageFile.PyDecoder):
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e: except struct.error as e:
raise OSError("Truncated DDS file") from e raise OSError("Truncated DDS file") from e
return 0, 0 return -1, 0
class DXT5Decoder(ImageFile.PyDecoder): class DXT5Decoder(ImageFile.PyDecoder):
@ -262,7 +264,7 @@ class DXT5Decoder(ImageFile.PyDecoder):
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e: except struct.error as e:
raise OSError("Truncated DDS file") from e raise OSError("Truncated DDS file") from e
return 0, 0 return -1, 0
Image.register_decoder("DXT1", DXT1Decoder) Image.register_decoder("DXT1", DXT1Decoder)

View File

@ -24,8 +24,6 @@ attribute will be ``None``.
Fully supported formats Fully supported formats
----------------------- -----------------------
.. contents::
BLP BLP
^^^ ^^^
@ -44,8 +42,9 @@ BMP
^^^ ^^^
Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``, Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``,
or ``RGB`` data. 16-colour images are read as ``P`` images. Run-length encoding or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length encoding
is not supported. is not supported. Support for reading 8-bit run-length encoding was added in Pillow
9.1.0.
The :py:meth:`~PIL.Image.open` method sets the following The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties: :py:attr:`~PIL.Image.Image.info` properties:
@ -106,8 +105,34 @@ writes run-length encoded files in GIF87a by default, unless GIF89a features
are used or GIF89a is already in use. are used or GIF89a is already in use.
GIF files are initially read as grayscale (``L``) or palette mode (``P``) GIF files are initially read as grayscale (``L``) or palette mode (``P``)
images, but seeking to later frames in an image will change the mode to either images. Seeking to later frames in a ``P`` image will change the image to
``RGB`` or ``RGBA``, depending on whether the first frame had transparency. ``RGB`` (or ``RGBA`` if the first frame had transparency).
``P`` mode images are changed to ``RGB`` because each frame of a GIF may contain
its own individual palette of up to 256 colors. When a new frame is placed onto a
previous frame, those colors may combine to exceed the ``P`` mode limit of 256
colors. Instead, the image is converted to ``RGB`` handle this.
If you would prefer the first ``P`` image frame to be ``RGB`` as well, so that
every ``P`` frame is converted to ``RGB`` or ``RGBA`` mode, there is a setting
available::
from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
GIF frames do not always contain individual palettes however. If there is only
a global palette, then all of the colors can fit within ``P`` mode. If you would
prefer the frames to be kept as ``P`` in that case, there is also a setting
available::
from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
To restore the default behavior, where ``P`` mode images are only converted to
``RGB`` or ``RGBA`` after the first frame::
from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
The :py:meth:`~PIL.Image.open` method sets the following The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties: :py:attr:`~PIL.Image.Image.info` properties:
@ -364,10 +389,12 @@ The :py:meth:`~PIL.Image.open` method may set the following
The :py:meth:`~PIL.Image.Image.save` method supports the following options: The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**quality** **quality**
The image quality, on a scale from 0 (worst) to 95 (best). The default is The image quality, on a scale from 0 (worst) to 95 (best), or the string
75. Values above 95 should be avoided; 100 disables portions of the JPEG ``keep``. The default is 75. Values above 95 should be avoided; 100 disables
compression algorithm, and results in large files with hardly any gain in portions of the JPEG compression algorithm, and results in large files with
image quality. hardly any gain in image quality. The value ``keep`` is only valid for JPEG
files and will retain the original image quality level, subsampling, and
qtables.
**optimize** **optimize**
If present and true, indicates that the encoder should make an extra pass If present and true, indicates that the encoder should make an extra pass
@ -475,9 +502,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
and must be greater than the code-block size. and must be greater than the code-block size.
**irreversible** **irreversible**
If ``True``, use the lossy Irreversible Color Transformation If ``True``, use the lossy discrete waveform transformation DWT 9-7.
followed by DWT 9-7. Defaults to ``False``, which means to use the Defaults to ``False``, which uses the lossless DWT 5-3.
Reversible Color Transformation with DWT 5-3.
**mct**
If ``1`` then enable multiple component transformation when encoding,
otherwise use ``0`` for no component transformation (default). If MCT is
enabled and ``irreversible`` is ``True`` then the Irreversible Color
Transformation will be applied, otherwise encoding will use the
Reversible Color Transformation. MCT works best with a ``mode`` of
``RGB`` and is only applicable when the image data has 3 components.
.. versionadded:: 9.1.0
**progression** **progression**
Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``, Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``,
@ -497,6 +533,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
for compliant 4K files, *at least one* of the dimensions must match for compliant 4K files, *at least one* of the dimensions must match
4096 x 2160. 4096 x 2160.
**no_jp2**
If ``True`` then don't wrap the raw codestream in the JP2 file format when
saving, otherwise the extension of the filename will be used to determine
the format (default).
.. versionadded:: 9.1.0
.. note:: .. note::
To enable JPEG 2000 support, you need to build and install the OpenJPEG To enable JPEG 2000 support, you need to build and install the OpenJPEG
@ -743,7 +786,7 @@ parameter must be set to ``True``. The following parameters can also be set:
PPM PPM
^^^ ^^^
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L`` or Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or
``RGB`` data. ``RGB`` data.
SGI SGI

View File

@ -171,20 +171,37 @@ Rolling an image
:: ::
def roll(image, delta): def roll(im, delta):
"""Roll an image sideways.""" """Roll an image sideways."""
xsize, ysize = image.size xsize, ysize = im.size
delta = delta % xsize delta = delta % xsize
if delta == 0: if delta == 0:
return image return im
part1 = image.crop((0, 0, delta, ysize)) part1 = im.crop((0, 0, delta, ysize))
part2 = image.crop((delta, 0, xsize, ysize)) part2 = im.crop((delta, 0, xsize, ysize))
image.paste(part1, (xsize - delta, 0, xsize, ysize)) im.paste(part1, (xsize - delta, 0, xsize, ysize))
image.paste(part2, (0, 0, xsize - delta, ysize)) im.paste(part2, (0, 0, xsize - delta, ysize))
return image return im
Or if you would like to merge two images into a wider image:
Merging images
^^^^^^^^^^^^^^
::
def merge(im1, im2):
w = im1.size[0] + im2.size[0]
h = max(im1.size[1], im2.size[1])
im = Image.new("RGBA", (w, h))
im.paste(im1)
im.paste(im2, (im1.size[0], 0))
return im
For more advanced tricks, the paste method can also take a transparency mask as For more advanced tricks, the paste method can also take a transparency mask as
an optional argument. In this mask, the value 255 indicates that the pasted an optional argument. In this mask, the value 255 indicates that the pasted

View File

@ -123,8 +123,12 @@ The ``tile`` attribute
To be able to read the file as well as just identifying it, the ``tile`` To be able to read the file as well as just identifying it, the ``tile``
attribute must also be set. This attribute consists of a list of tile attribute must also be set. This attribute consists of a list of tile
descriptors, where each descriptor specifies how data should be loaded to a descriptors, where each descriptor specifies how data should be loaded to a
given region in the image. In most cases, only a single descriptor is used, given region in the image.
covering the full image.
In most cases, only a single descriptor is used, covering the full image.
:py:class:`.PsdImagePlugin.PsdImageFile` uses multiple tiles to combine
channels within a single layer, given that the channels are stored separately,
one after the other.
The tile descriptor is a 4-tuple with the following contents:: The tile descriptor is a 4-tuple with the following contents::
@ -324,42 +328,42 @@ The fields are used as follows:
Whether the first line in the image is the top line on the screen (1), or Whether the first line in the image is the top line on the screen (1), or
the bottom line (-1). If omitted, the orientation defaults to 1. the bottom line (-1). If omitted, the orientation defaults to 1.
.. _file-decoders: .. _file-codecs:
Writing Your Own File Decoder in C Writing Your Own File Codec in C
================================== ================================
There are 3 stages in a file decoder's lifetime: There are 3 stages in a file codec's lifetime:
1. Setup: Pillow looks for a function in the decoder registry, falling 1. Setup: Pillow looks for a function in the decoder or encoder registry,
back to a function named ``[decodername]_decoder`` on the internal falling back to a function named ``[codecname]_decoder`` or
core image object. That function is called with the ``args`` tuple ``[codecname]_encoder`` on the internal core image object. That function is
from the ``tile`` setup in the ``_open`` method. called with the ``args`` tuple from the ``tile``.
2. Decoding: The decoder's decode function is repeatedly called with 2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly
chunks of image data. called with chunks of image data.
3. Cleanup: If the decoder has registered a cleanup function, it will 3. Cleanup: If the codec has registered a cleanup function, it will
be called at the end of the decoding process, even if there was an be called at the end of the transformation process, even if there was an
exception raised. exception raised.
Setup Setup
----- -----
The current conventions are that the decoder setup function is named The current conventions are that the codec setup function is named
``PyImaging_[Decodername]DecoderNew`` and defined in ``decode.c``. The ``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew``
python binding for it is named ``[decodername]_decoder`` and is setup and defined in ``decode.c`` or ``encode.c``. The Python binding for it is
from within the ``_imaging.c`` file in the codecs section of the named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from
function array. within the ``_imaging.c`` file in the codecs section of the function array.
The setup function needs to call ``PyImaging_DecoderNew`` and at the The setup function needs to call ``PyImaging_DecoderNew`` or
very least, set the ``decode`` function pointer. The fields of ``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or
interest in this object are: ``encode`` function pointer. The fields of interest in this object are:
**decode** **decode**/**encode**
Function pointer to the decode function, which has access to Function pointer to the decode or encode function, which has access to
``im``, ``state``, and the buffer of data to be added to the image. ``im``, ``state``, and the buffer of data to be transformed.
**cleanup** **cleanup**
Function pointer to the cleanup function, has access to ``state``. Function pointer to the cleanup function, has access to ``state``.
@ -369,36 +373,34 @@ interest in this object are:
**state** **state**
An ImagingCodecStateInstance, will be set by Pillow. The ``context`` An ImagingCodecStateInstance, will be set by Pillow. The ``context``
member is an opaque struct that can be used by the decoder to store member is an opaque struct that can be used by the codec to store
any format specific state or options. any format specific state or options.
**pulls_fd** **pulls_fd**/**pushes_fd**
**EXPERIMENTAL** -- **WARNING**, interface may change. If set to 1, If the decoder has ``pulls_fd`` or the encoder has ``pushes_fd`` set to 1,
``state->fd`` will be a pointer to the Python file like object. The ``state->fd`` will be a pointer to the Python file like object. The codec may
decoder may use the functions in ``codec_fd.c`` to read directly use the functions in ``codec_fd.c`` to read or write directly with the file
from the file like object rather than have the data pushed through a like object rather than have the data pushed through a buffer.
buffer. Note that this implementation may be refactored until this
warning is removed.
.. versionadded:: 3.3.0 .. versionadded:: 3.3.0
Decoding Transforming
-------- ------------
The decode function is called with the target (core) image, the The decode or encode function is called with the target (core) image, the codec
decoder state structure, and a buffer of data to be decoded. state structure, and a buffer of data to be transformed.
**Experimental** -- If ``pulls_fd`` is set, then the decode function It is the codec's responsibility to pull as much data as possible out of the
is called once, with an empty buffer. It is the decoder's buffer and return the number of bytes consumed. The next call to the codec will
responsibility to decode the entire tile in that one call. The rest of include the previous unconsumed tail. The codec function will be called
this section only applies if ``pulls_fd`` is not set. multiple times as the data processed.
It is the decoder's responsibility to pull as much data as possible Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or
out of the buffer and return the number of bytes consumed. The next encode function is called once, with an empty buffer. It is the codec's
call to the decoder will include the previous unconsumed tail. The responsibility to transform the entire tile in that one call. Using this will
decoder function will be called multiple times as the data is read provide a codec with more freedom, but that freedom may mean increased memory
from the file like object. usage if the entire tile is held in memory at once by the codec.
If an error occurs, set ``state->errcode`` and return -1. If an error occurs, set ``state->errcode`` and return -1.
@ -407,10 +409,9 @@ Return -1 on success, without setting the errcode.
Cleanup Cleanup
------- -------
The cleanup function is called after the decoder returns a negative The cleanup function is called after the codec returns a negative
value, or if there is a read error from the file. This function should value, or if there is an error. This function should free any allocated
free any allocated memory and release any resources from external memory and release any resources from external libraries.
libraries.
.. _file-codecs-py: .. _file-codecs-py:
@ -425,11 +426,32 @@ They should be registered using :py:meth:`PIL.Image.register_decoder` and
the file codecs, there are three stages in the lifetime of a the file codecs, there are three stages in the lifetime of a
Python-based file codec: Python-based file codec:
1. Setup: Pillow looks for the decoder in the registry, then 1. Setup: Pillow looks for the codec in the decoder or encoder registry, then
instantiates the class. instantiates the class.
2. Transforming: The instance's ``decode`` method is repeatedly called with 2. Transforming: The instance's ``decode`` method is repeatedly called with
a buffer of data to be interpreted, or the ``encode`` method is repeatedly a buffer of data to be interpreted, or the ``encode`` method is repeatedly
called with the size of data to be output. called with the size of data to be output.
3. Cleanup: The instance's ``cleanup`` method is called. Alternatively, if the decoder's ``_pulls_fd`` property (or the encoder's
``_pushes_fd`` property) is set to ``True``, then ``decode`` and ``encode``
will only be called once. In the decoder, ``self.fd`` can be used to access
the file-like object. Using this will provide a codec with more freedom, but
that freedom may mean increased memory usage if entire file is held in
memory at once by the codec.
In ``decode``, once the data has been interpreted, ``set_as_raw`` can be
used to populate the image.
3. Cleanup: The instance's ``cleanup`` method is called once the transformation
is complete. This can be used to clean up any resources used by the codec.
If you set ``_pulls_fd`` or ``_pushes_fd`` to ``True`` however, then you
probably chose to perform any cleanup tasks at the end of ``decode`` or
``encode``.
For an example :py:class:`PIL.ImageFile.PyDecoder`, see `DdsImagePlugin
<https://github.com/python-pillow/Pillow/blob/main/docs/example/DdsImagePlugin.py>`_.
For a plugin that uses both :py:class:`PIL.ImageFile.PyDecoder` and
:py:class:`PIL.ImageFile.PyEncoder`, see `BlpImagePlugin
<https://github.com/python-pillow/Pillow/blob/main/src/PIL/BlpImagePlugin.py>`_

View File

@ -461,8 +461,6 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86 | | Debian 11 Bullseye | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 34 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 35 | 3.10 | x86-64 | | Fedora 35 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 | | Gentoo | 3.9 | x86-64 |

View File

@ -14,6 +14,16 @@ for a region of an image.
statistics. You can also pass in a previously calculated histogram. statistics. You can also pass in a previously calculated histogram.
:param image: A PIL image, or a precalculated histogram. :param image: A PIL image, or a precalculated histogram.
.. note::
For a PIL image, calculations rely on the
:py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are
grouped into 256 bins, even if the image has more than 8 bits per
channel. So ``I`` and ``F`` mode images have a maximum ``mean``,
``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum
of more than 255.
:param mask: An optional mask. :param mask: An optional mask.
.. py:attribute:: extrema .. py:attribute:: extrema

View File

@ -6,7 +6,13 @@
The PixelAccess class provides read and write access to The PixelAccess class provides read and write access to
:py:class:`PIL.Image` data at a pixel level. :py:class:`PIL.Image` data at a pixel level.
.. note:: Accessing individual pixels is fairly slow. If you are looping over all of the pixels in an image, there is likely a faster way using other parts of the Pillow API. .. note:: Accessing individual pixels is fairly slow. If you are
looping over all of the pixels in an image, there is likely
a faster way using other parts of the Pillow API.
:mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps`
have methods for many standard operations. If you wish to perform
a custom mapping, check out :py:meth:`~PIL.Image.Image.point`.
Example Example
------- -------
@ -39,7 +45,7 @@ Access using negative indexes is also possible.
:py:class:`PixelAccess` Class :py:class:`PixelAccess` Class
----------------------------------- -----------------------------
.. class:: PixelAccess .. class:: PixelAccess

View File

@ -7,8 +7,12 @@
The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version. The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version.
.. note:: Accessing individual pixels is fairly slow. If you are .. note:: Accessing individual pixels is fairly slow. If you are
looping over all of the pixels in an image, there is likely looping over all of the pixels in an image, there is likely
a faster way using other parts of the Pillow API. a faster way using other parts of the Pillow API.
:mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps`
have methods for many standard operations. If you wish to perform
a custom mapping, check out :py:meth:`~PIL.Image.Image.point`.
Example Example
------- -------

View File

@ -146,12 +146,24 @@ At present, the information within each block is merely returned as a dictionary
"data" entry. This will allow more useful information to be added in the future without "data" entry. This will allow more useful information to be added in the future without
breaking backwards compatibility. breaking backwards compatibility.
Added rawmode argument to Image.getpalette() Added mct and no_jp2 options for saving JPEG 2000
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
By default, :py:meth:`~PIL.Image.Image.getpalette` returns RGB data from the palette. The :py:meth:`PIL.Image.Image.save` method now supports the following options for
A ``rawmode`` argument has been added, to allow the mode to be chosen instead. ``None`` JPEG 2000:
can be used to return data in the current mode of the palette.
**mct**
If ``1`` then enable multiple component transformation when encoding,
otherwise use ``0`` for no component transformation (default). If MCT is
enabled and ``irreversible`` is ``True`` then the Irreversible Color
Transformation will be applied, otherwise encoding will use the
Reversible Color Transformation. MCT works best with a ``mode`` of
``RGB`` and is only applicable when the image data has 3 components.
**no_jp2**
If ``True`` then don't wrap the raw codestream in the JP2 file format when
saving, otherwise the extension of the filename will be used to determine
the format (default).
Added PyEncoder Added PyEncoder
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
@ -160,9 +172,35 @@ Added PyEncoder
written in Python. See :ref:`Writing Your Own File Codec in Python<file-codecs-py>` for written in Python. See :ref:`Writing Your Own File Codec in Python<file-codecs-py>` for
more information. more information.
GifImagePlugin loading strategy
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This
behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as
well.
.. code-block:: python
from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
Or subsequent frames can be kept in ``P`` mode as long as there is only a single
palette.
.. code-block:: python
from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
Other Changes Other Changes
============= =============
musllinux wheels
^^^^^^^^^^^^^^^^
Pillow now builds binary wheels for musllinux, suitable for Linux distributions based on the musl C standard library such as Alpine
(rather than the glibc library used by manylinux wheels). See :pep:`656`.
ImageShow temporary files on Unix ImageShow temporary files on Unix
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -177,6 +215,11 @@ Image._repr_pretty_
identity of the object. This allows Jupyter to describe an image and have that identity of the object. This allows Jupyter to describe an image and have that
description stay the same on subsequent executions of the same code. description stay the same on subsequent executions of the same code.
Added BigTIFF reading
^^^^^^^^^^^^^^^^^^^^^
Support has been added for reading BigTIFF images.
Added BLP saving Added BLP saving
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^

File diff suppressed because it is too large Load Diff

View File

@ -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;
}
}

View File

@ -1,8 +0,0 @@
th p {
margin-bottom: 0;
}
.rst-content tr .line-block {
font-size: 1rem;
margin-bottom: 0;
}

View File

@ -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');
});
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -37,12 +37,12 @@ python_requires = >=3.7
[options.extras_require] [options.extras_require]
docs = docs =
furo
olefile olefile
sphinx>=2.4 sphinx>=2.4
sphinx-copybutton sphinx-copybutton
sphinx-issues>=3.0.1 sphinx-issues>=3.0.1
sphinx-removed-in sphinx-removed-in
sphinx-rtd-theme>=1.0
sphinxext-opengraph sphinxext-opengraph
tests = tests =
check-manifest check-manifest

View File

@ -167,7 +167,7 @@ def _find_library_dirs_ldconfig():
# Assuming GLIBC's ldconfig (with option -p) # Assuming GLIBC's ldconfig (with option -p)
# Alpine Linux uses musl that can't print cache # Alpine Linux uses musl that can't print cache
args = ["/sbin/ldconfig", "-p"] args = ["/sbin/ldconfig", "-p"]
expr = fr".*\({abi_type}.*\) => (.*)" expr = rf".*\({abi_type}.*\) => (.*)"
env = dict(os.environ) env = dict(os.environ)
env["LC_ALL"] = "C" env["LC_ALL"] = "C"
env["LANG"] = "C" env["LANG"] = "C"

View File

@ -306,7 +306,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
self._load() self._load()
except struct.error as e: except struct.error as e:
raise OSError("Truncated BLP file") from e raise OSError("Truncated BLP file") from e
return 0, 0 return -1, 0
def _read_blp_header(self): def _read_blp_header(self):
self.fd.seek(4) self.fd.seek(4)

View File

@ -24,6 +24,8 @@
# #
import os
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16 from ._binary import i16le as i16
from ._binary import i32le as i32 from ._binary import i32le as i32
@ -102,7 +104,7 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["height"] = ( file_info["height"] = (
i32(header_data, 4) i32(header_data, 4)
if not file_info["y_flip"] if not file_info["y_flip"]
else 2 ** 32 - i32(header_data, 4) else 2**32 - i32(header_data, 4)
) )
file_info["planes"] = i16(header_data, 8) file_info["planes"] = i16(header_data, 8)
file_info["bits"] = i16(header_data, 10) file_info["bits"] = i16(header_data, 10)
@ -167,6 +169,7 @@ class BmpImageFile(ImageFile.ImageFile):
raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})") raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})")
# ---------------- Process BMP with Bitfields compression (not palette) # ---------------- Process BMP with Bitfields compression (not palette)
decoder_name = "raw"
if file_info["compression"] == self.BITFIELDS: if file_info["compression"] == self.BITFIELDS:
SUPPORTED = { SUPPORTED = {
32: [ 32: [
@ -208,6 +211,8 @@ class BmpImageFile(ImageFile.ImageFile):
elif file_info["compression"] == self.RAW: elif file_info["compression"] == self.RAW:
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
raw_mode, self.mode = "BGRA", "RGBA" raw_mode, self.mode = "BGRA", "RGBA"
elif file_info["compression"] == self.RLE8:
decoder_name = "bmp_rle"
else: else:
raise OSError(f"Unsupported BMP compression ({file_info['compression']})") raise OSError(f"Unsupported BMP compression ({file_info['compression']})")
@ -247,7 +252,7 @@ class BmpImageFile(ImageFile.ImageFile):
self.info["compression"] = file_info["compression"] self.info["compression"] = file_info["compression"]
self.tile = [ self.tile = [
( (
"raw", decoder_name,
(0, 0, file_info["width"], file_info["height"]), (0, 0, file_info["width"], file_info["height"]),
offset or self.fp.tell(), offset or self.fp.tell(),
( (
@ -271,6 +276,57 @@ class BmpImageFile(ImageFile.ImageFile):
self._bitmap(offset=offset) self._bitmap(offset=offset)
class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
data = bytearray()
x = 0
while len(data) < self.state.xsize * self.state.ysize:
pixels = self.fd.read(1)
byte = self.fd.read(1)
if not pixels or not byte:
break
num_pixels = pixels[0]
if num_pixels:
# encoded mode
if x + num_pixels > self.state.xsize:
# Too much data for row
num_pixels = max(0, self.state.xsize - x)
data += byte * num_pixels
x += num_pixels
else:
if byte[0] == 0:
# end of line
while len(data) % self.state.xsize != 0:
data += b"\x00"
x = 0
elif byte[0] == 1:
# end of bitmap
break
elif byte[0] == 2:
# delta
bytes_read = self.fd.read(2)
if len(bytes_read) < 2:
break
right, up = self.fd.read(2)
data += b"\x00" * (right + up * self.state.xsize)
x = len(data) % self.state.xsize
else:
# absolute mode
bytes_read = self.fd.read(byte[0])
data += bytes_read
if len(bytes_read) < byte[0]:
break
x += byte[0]
# align to 16-bit word boundary
if self.fd.tell() % 2 != 0:
self.fd.seek(1, os.SEEK_CUR)
self.set_as_raw(bytes(data), ("P", 0, self.args[-1]))
return -1, 0
# ============================================================================= # =============================================================================
# Image plugin for the DIB format (BMP alias) # Image plugin for the DIB format (BMP alias)
# ============================================================================= # =============================================================================
@ -322,7 +378,7 @@ def _save(im, fp, filename, bitmap_header=True):
if bitmap_header: if bitmap_header:
offset = 14 + header + colors * 4 offset = 14 + header + colors * 4
file_size = offset + image file_size = offset + image
if file_size > 2 ** 32 - 1: if file_size > 2**32 - 1:
raise ValueError("File size is too large for the BMP format") raise ValueError("File size is too large for the BMP format")
fp.write( fp.write(
b"BM" # file type (magic) b"BM" # file type (magic)
@ -372,6 +428,8 @@ Image.register_extension(BmpImageFile.format, ".bmp")
Image.register_mime(BmpImageFile.format, "image/bmp") Image.register_mime(BmpImageFile.format, "image/bmp")
Image.register_decoder("bmp_rle", BmpRleDecoder)
Image.register_open(DibImageFile.format, DibImageFile, _dib_accept) Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
Image.register_save(DibImageFile.format, _dib_save) Image.register_save(DibImageFile.format, _dib_save)

View File

@ -111,7 +111,9 @@ class DdsImageFile(ImageFile.ImageFile):
format_description = "DirectDraw Surface" format_description = "DirectDraw Surface"
def _open(self): def _open(self):
magic, header_size = struct.unpack("<II", self.fp.read(8)) if not _accept(self.fp.read(4)):
raise SyntaxError("not a DDS file")
(header_size,) = struct.unpack("<I", self.fp.read(4))
if header_size != 124: if header_size != 124:
raise OSError(f"Unsupported header size {repr(header_size)}") raise OSError(f"Unsupported header size {repr(header_size)}")
header_bytes = self.fp.read(header_size - 4) header_bytes = self.fp.read(header_size - 4)

View File

@ -26,7 +26,11 @@ from ._binary import o8
def _accept(prefix): def _accept(prefix):
return len(prefix) >= 6 and i16(prefix, 4) in [0xAF11, 0xAF12] return (
len(prefix) >= 6
and i16(prefix, 4) in [0xAF11, 0xAF12]
and i16(prefix, 14) in [0, 3] # flags
)
## ##
@ -44,11 +48,7 @@ class FliImageFile(ImageFile.ImageFile):
# HEAD # HEAD
s = self.fp.read(128) s = self.fp.read(128)
if not ( if not (_accept(s) and s[20:22] == b"\x00\x00"):
_accept(s)
and i16(s, 14) in [0, 3] # flags
and s[20:22] == b"\x00\x00" # reserved
):
raise SyntaxError("not an FLI/FLC file") raise SyntaxError("not an FLI/FLC file")
# frames # frames

View File

@ -94,7 +94,8 @@ class FtexImageFile(ImageFile.ImageFile):
format_description = "Texture File Format (IW2:EOC)" format_description = "Texture File Format (IW2:EOC)"
def _open(self): def _open(self):
struct.unpack("<I", self.fp.read(4)) # magic if not _accept(self.fp.read(4)):
raise SyntaxError("not an FTEX file")
struct.unpack("<i", self.fp.read(4)) # version struct.unpack("<i", self.fp.read(4)) # version
self._size = struct.unpack("<2i", self.fp.read(8)) self._size = struct.unpack("<2i", self.fp.read(8))
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8)) mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))

View File

@ -43,9 +43,9 @@ class GbrImageFile(ImageFile.ImageFile):
def _open(self): def _open(self):
header_size = i32(self.fp.read(4)) header_size = i32(self.fp.read(4))
version = i32(self.fp.read(4))
if header_size < 20: if header_size < 20:
raise SyntaxError("not a GIMP brush") raise SyntaxError("not a GIMP brush")
version = i32(self.fp.read(4))
if version not in (1, 2): if version not in (1, 2):
raise SyntaxError(f"Unsupported GIMP brush version: {version}") raise SyntaxError(f"Unsupported GIMP brush version: {version}")

View File

@ -28,12 +28,25 @@ import itertools
import math import math
import os import os
import subprocess import subprocess
from enum import IntEnum
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16le as i16 from ._binary import i16le as i16
from ._binary import o8 from ._binary import o8
from ._binary import o16le as o16 from ._binary import o16le as o16
class LoadingStrategy(IntEnum):
""".. versionadded:: 9.1.0"""
RGB_AFTER_FIRST = 0
RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1
RGB_ALWAYS = 2
#: .. versionadded:: 9.1.0
LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Identify/read GIF files # Identify/read GIF files
@ -61,6 +74,12 @@ class GifImageFile(ImageFile.ImageFile):
return self.fp.read(s[0]) return self.fp.read(s[0])
return None return None
def _is_palette_needed(self, p):
for i in range(0, len(p), 3):
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
return True
return False
def _open(self): def _open(self):
# Screen # Screen
@ -79,11 +98,9 @@ class GifImageFile(ImageFile.ImageFile):
self.info["background"] = s[11] self.info["background"] = s[11]
# check if palette contains colour indices # check if palette contains colour indices
p = self.fp.read(3 << bits) p = self.fp.read(3 << bits)
for i in range(0, len(p), 3): if self._is_palette_needed(p):
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]): p = ImagePalette.raw("RGB", p)
p = ImagePalette.raw("RGB", p) self.global_palette = self.palette = p
self.global_palette = self.palette = p
break
self.__fp = self.fp # FIXME: hack self.__fp = self.fp # FIXME: hack
self.__rewind = self.fp.tell() self.__rewind = self.fp.tell()
@ -97,7 +114,7 @@ class GifImageFile(ImageFile.ImageFile):
current = self.tell() current = self.tell()
try: try:
while True: while True:
self.seek(self.tell() + 1) self._seek(self.tell() + 1, False)
except EOFError: except EOFError:
self._n_frames = self.tell() + 1 self._n_frames = self.tell() + 1
self.seek(current) self.seek(current)
@ -110,14 +127,16 @@ class GifImageFile(ImageFile.ImageFile):
self._is_animated = self._n_frames != 1 self._is_animated = self._n_frames != 1
else: else:
current = self.tell() current = self.tell()
if current:
try:
self.seek(1)
self._is_animated = True self._is_animated = True
except EOFError: else:
self._is_animated = False try:
self._seek(1, False)
self._is_animated = True
except EOFError:
self._is_animated = False
self.seek(current) self.seek(current)
return self._is_animated return self._is_animated
def seek(self, frame): def seek(self, frame):
@ -135,26 +154,22 @@ class GifImageFile(ImageFile.ImageFile):
self.seek(last_frame) self.seek(last_frame)
raise EOFError("no more images in GIF file") from e raise EOFError("no more images in GIF file") from e
def _seek(self, frame): def _seek(self, frame, update_image=True):
if frame == 0: if frame == 0:
# rewind # rewind
self.__offset = 0 self.__offset = 0
self.dispose = None self.dispose = None
self.dispose_extent = [0, 0, 0, 0] # x0, y0, x1, y1
self.__frame = -1 self.__frame = -1
self.__fp.seek(self.__rewind) self.__fp.seek(self.__rewind)
self.disposal_method = 0 self.disposal_method = 0
else: else:
# ensure that the previous frame was loaded # ensure that the previous frame was loaded
if self.tile: if self.tile and update_image:
self.load() self.load()
if frame != self.__frame + 1: if frame != self.__frame + 1:
raise ValueError(f"cannot seek to frame {frame}") raise ValueError(f"cannot seek to frame {frame}")
self.__frame = frame
self.tile = []
self.fp = self.__fp self.fp = self.__fp
if self.__offset: if self.__offset:
@ -164,28 +179,24 @@ class GifImageFile(ImageFile.ImageFile):
pass pass
self.__offset = 0 self.__offset = 0
if self.__frame == 1: s = self.fp.read(1)
self.pyaccess = None if not s or s == b";":
if "transparency" in self.info: raise EOFError
self.mode = "RGBA"
self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
del self.info["transparency"] self.__frame = frame
else:
self.mode = "RGB" self.tile = []
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
if self.dispose:
self.im.paste(self.dispose, self.dispose_extent)
palette = None palette = None
info = {} info = {}
frame_transparency = None frame_transparency = None
interlace = None interlace = None
frame_dispose_extent = None
while True: while True:
s = self.fp.read(1) if not s:
s = self.fp.read(1)
if not s or s == b";": if not s or s == b";":
break break
@ -223,6 +234,7 @@ class GifImageFile(ImageFile.ImageFile):
else: else:
info["comment"] = block info["comment"] = block
block = self.data() block = self.data()
s = None
continue continue
elif s[0] == 255: elif s[0] == 255:
# #
@ -245,16 +257,18 @@ class GifImageFile(ImageFile.ImageFile):
# extent # extent
x0, y0 = i16(s, 0), i16(s, 2) x0, y0 = i16(s, 0), i16(s, 2)
x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6) x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
if x1 > self.size[0] or y1 > self.size[1]: if (x1 > self.size[0] or y1 > self.size[1]) and update_image:
self._size = max(x1, self.size[0]), max(y1, self.size[1]) self._size = max(x1, self.size[0]), max(y1, self.size[1])
self.dispose_extent = x0, y0, x1, y1 frame_dispose_extent = x0, y0, x1, y1
flags = s[8] flags = s[8]
interlace = (flags & 64) != 0 interlace = (flags & 64) != 0
if flags & 128: if flags & 128:
bits = (flags & 7) + 1 bits = (flags & 7) + 1
palette = ImagePalette.raw("RGB", self.fp.read(3 << bits)) p = self.fp.read(3 << bits)
if self._is_palette_needed(p):
palette = ImagePalette.raw("RGB", p)
# image data # image data
bits = self.fp.read(1)[0] bits = self.fp.read(1)[0]
@ -264,16 +278,56 @@ class GifImageFile(ImageFile.ImageFile):
else: else:
pass pass
# raise OSError, "illegal GIF tag `%x`" % s[0] # raise OSError, "illegal GIF tag `%x`" % s[0]
s = None
frame_palette = palette or self.global_palette if interlace is None:
# self.__fp = None
raise EOFError
if not update_image:
return
if self.dispose:
self.im.paste(self.dispose, self.dispose_extent)
self._frame_palette = palette or self.global_palette
if frame == 0:
if self._frame_palette:
self.mode = (
"RGB" if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS else "P"
)
else:
self.mode = "L"
if not palette and self.global_palette:
from copy import copy
palette = copy(self.global_palette)
self.palette = palette
else:
self._frame_transparency = frame_transparency
if self.mode == "P":
if (
LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
or palette
):
self.pyaccess = None
if "transparency" in self.info:
self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
self.mode = "RGBA"
del self.info["transparency"]
else:
self.mode = "RGB"
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
def _rgb(color): def _rgb(color):
if frame_palette: if self._frame_palette:
color = tuple(frame_palette.palette[color * 3 : color * 3 + 3]) color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
else: else:
color = (color, color, color) color = (color, color, color)
return color return color
self.dispose_extent = frame_dispose_extent
try: try:
if self.disposal_method < 2: if self.disposal_method < 2:
# do not dispose or none specified # do not dispose or none specified
@ -288,17 +342,21 @@ class GifImageFile(ImageFile.ImageFile):
Image._decompression_bomb_check(dispose_size) Image._decompression_bomb_check(dispose_size)
# by convention, attempt to use transparency first # by convention, attempt to use transparency first
dispose_mode = "P"
color = self.info.get("transparency", frame_transparency) color = self.info.get("transparency", frame_transparency)
if color is not None: if color is not None:
dispose_mode = "RGBA" if self.mode in ("RGB", "RGBA"):
color = _rgb(color) + (0,) dispose_mode = "RGBA"
color = _rgb(color) + (0,)
else: else:
dispose_mode = "RGB" color = self.info.get("background", 0)
color = _rgb(self.info.get("background", 0)) if self.mode in ("RGB", "RGBA"):
dispose_mode = "RGB"
color = _rgb(color)
self.dispose = Image.core.fill(dispose_mode, dispose_size, color) self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
else: else:
# replace with previous contents # replace with previous contents
if self.im: if self.im is not None:
# only dispose the extent in this frame # only dispose the extent in this frame
self.dispose = self._crop(self.im, self.dispose_extent) self.dispose = self._crop(self.im, self.dispose_extent)
elif frame_transparency is not None: elif frame_transparency is not None:
@ -306,26 +364,30 @@ class GifImageFile(ImageFile.ImageFile):
dispose_size = (x1 - x0, y1 - y0) dispose_size = (x1 - x0, y1 - y0)
Image._decompression_bomb_check(dispose_size) Image._decompression_bomb_check(dispose_size)
self.dispose = Image.core.fill( dispose_mode = "P"
"RGBA", dispose_size, _rgb(frame_transparency) + (0,) color = frame_transparency
) if self.mode in ("RGB", "RGBA"):
dispose_mode = "RGBA"
color = _rgb(frame_transparency) + (0,)
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
except AttributeError: except AttributeError:
pass pass
if interlace is not None: if interlace is not None:
if frame == 0 and frame_transparency is not None: transparency = -1
self.info["transparency"] = frame_transparency if frame_transparency is not None:
if frame == 0:
self.info["transparency"] = frame_transparency
elif self.mode not in ("RGB", "RGBA"):
transparency = frame_transparency
self.tile = [ self.tile = [
( (
"gif", "gif",
(x0, y0, x1, y1), (x0, y0, x1, y1),
self.__offset, self.__offset,
(bits, interlace), (bits, interlace, transparency),
) )
] ]
else:
# self.__fp = None
raise EOFError
for k in ["duration", "comment", "extension", "loop"]: for k in ["duration", "comment", "extension", "loop"]:
if k in info: if k in info:
@ -333,45 +395,42 @@ class GifImageFile(ImageFile.ImageFile):
elif k in self.info: elif k in self.info:
del self.info[k] del self.info[k]
if frame == 0:
self.mode = "P" if frame_palette else "L"
if self.mode == "P" and not palette:
from copy import copy
palette = copy(self.global_palette)
self.palette = palette
else:
self._frame_palette = frame_palette
self._frame_transparency = frame_transparency
def load_prepare(self): def load_prepare(self):
temp_mode = "P" if self._frame_palette else "L"
self._prev_im = None
if self.__frame == 0: if self.__frame == 0:
if "transparency" in self.info: if "transparency" in self.info:
self.im = Image.core.fill( self.im = Image.core.fill(
self.mode, self.size, self.info["transparency"] temp_mode, self.size, self.info["transparency"]
) )
else: elif self.mode in ("RGB", "RGBA"):
self._prev_im = self.im self._prev_im = self.im
if self._frame_palette: if self._frame_palette:
self.mode = "P"
self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
self.im.putpalette(*self._frame_palette.getdata()) self.im.putpalette(*self._frame_palette.getdata())
self._frame_palette = None
else: else:
self.mode = "L"
self.im = None self.im = None
self.mode = temp_mode
self._frame_palette = None
super().load_prepare() super().load_prepare()
def load_end(self): def load_end(self):
if self.__frame == 0: if self.__frame == 0:
if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
self.mode = "RGB"
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
return return
if self._frame_transparency is not None: if self.mode == "P" and self._prev_im:
self.im.putpalettealpha(self._frame_transparency, 0) if self._frame_transparency is not None:
frame_im = self.im.convert("RGBA") self.im.putpalettealpha(self._frame_transparency, 0)
frame_im = self.im.convert("RGBA")
else:
frame_im = self.im.convert("RGB")
else: else:
frame_im = self.im.convert("RGB") if not self._prev_im:
return
frame_im = self.im
frame_im = self._crop(frame_im, self.dispose_extent) frame_im = self._crop(frame_im, self.dispose_extent)
self.im = self._prev_im self.im = self._prev_im
@ -401,7 +460,7 @@ class GifImageFile(ImageFile.ImageFile):
RAWMODE = {"1": "L", "L": "L", "P": "P"} RAWMODE = {"1": "L", "L": "L", "P": "P"}
def _normalize_mode(im, initial_call=False): def _normalize_mode(im):
""" """
Takes an image (or frame), returns an image in a mode that is appropriate Takes an image (or frame), returns an image in a mode that is appropriate
for saving in a Gif. for saving in a Gif.
@ -409,31 +468,20 @@ def _normalize_mode(im, initial_call=False):
It may return the original image, or it may return an image converted to It may return the original image, or it may return an image converted to
palette or 'L' mode. palette or 'L' mode.
UNDONE: What is the point of mucking with the initial call palette, for
an image that shouldn't have a palette, or it would be a mode 'P' and
get returned in the RAWMODE clause.
:param im: Image object :param im: Image object
:param initial_call: Default false, set to true for a single frame.
:returns: Image object :returns: Image object
""" """
if im.mode in RAWMODE: if im.mode in RAWMODE:
im.load() im.load()
return im return im
if Image.getmodebase(im.mode) == "RGB": if Image.getmodebase(im.mode) == "RGB":
if initial_call: im = im.convert("P", palette=Image.Palette.ADAPTIVE)
palette_size = 256 if im.palette.mode == "RGBA":
if im.palette: for rgba in im.palette.colors.keys():
palette_size = len(im.palette.getdata()[1]) // 3 if rgba[3] == 0:
im = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=palette_size) im.info["transparency"] = im.palette.colors[rgba]
if im.palette.mode == "RGBA": break
for rgba in im.palette.colors.keys(): return im
if rgba[3] == 0:
im.info["transparency"] = im.palette.colors[rgba]
break
return im
else:
return im.convert("P")
return im.convert("L") return im.convert("L")
@ -491,7 +539,7 @@ def _normalize_palette(im, palette, info):
def _write_single_frame(im, fp, palette): def _write_single_frame(im, fp, palette):
im_out = _normalize_mode(im, True) im_out = _normalize_mode(im)
for k, v in im_out.info.items(): for k, v in im_out.info.items():
im.encoderinfo.setdefault(k, v) im.encoderinfo.setdefault(k, v)
im_out = _normalize_palette(im_out, palette, im.encoderinfo) im_out = _normalize_palette(im_out, palette, im.encoderinfo)
@ -623,11 +671,14 @@ def get_interlace(im):
def _write_local_header(fp, im, offset, flags): def _write_local_header(fp, im, offset, flags):
transparent_color_exists = False transparent_color_exists = False
try: try:
transparency = im.encoderinfo["transparency"] if "transparency" in im.encoderinfo:
except KeyError: transparency = im.encoderinfo["transparency"]
else:
transparency = im.info["transparency"]
transparency = int(transparency)
except (KeyError, ValueError):
pass pass
else: else:
transparency = int(transparency)
# optimize the block away if transparent color is not used # optimize the block away if transparent color is not used
transparent_color_exists = True transparent_color_exists = True

View File

@ -38,7 +38,7 @@ class GimpPaletteFile:
break break
# skip fields and comment lines # skip fields and comment lines
if re.match(br"\w+:|#", s): if re.match(rb"\w+:|#", s):
continue continue
if len(s) > 100: if len(s) > 100:
raise SyntaxError("bad palette file") raise SyntaxError("bad palette file")

View File

@ -167,7 +167,7 @@ class IcnsFile:
self.dct = dct = {} self.dct = dct = {}
self.fobj = fobj self.fobj = fobj
sig, filesize = nextheader(fobj) sig, filesize = nextheader(fobj)
if sig != MAGIC: if not _accept(sig):
raise SyntaxError("not an icns file") raise SyntaxError("not an icns file")
i = HEADERSIZE i = HEADERSIZE
while i < filesize: while i < filesize:
@ -287,7 +287,7 @@ class IcnsImageFile(ImageFile.ImageFile):
) )
px = Image.Image.load(self) px = Image.Image.load(self)
if self.im and self.im.size == self.size: if self.im is not None and self.im.size == self.size:
# Already loaded # Already loaded
return px return px
self.load_prepare() self.load_prepare()

View File

@ -22,7 +22,6 @@
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx # * https://msdn.microsoft.com/en-us/library/ms997538.aspx
import struct
import warnings import warnings
from io import BytesIO from io import BytesIO
from math import ceil, log from math import ceil, log
@ -30,6 +29,8 @@ from math import ceil, log
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
from ._binary import i16le as i16 from ._binary import i16le as i16
from ._binary import i32le as i32 from ._binary import i32le as i32
from ._binary import o8
from ._binary import o16le as o16
from ._binary import o32le as o32 from ._binary import o32le as o32
# #
@ -40,57 +41,72 @@ _MAGIC = b"\0\0\1\0"
def _save(im, fp, filename): def _save(im, fp, filename):
fp.write(_MAGIC) # (2+2) fp.write(_MAGIC) # (2+2)
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
sizes = im.encoderinfo.get( sizes = im.encoderinfo.get(
"sizes", "sizes",
[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
) )
frames = []
provided_ims = [im] + im.encoderinfo.get("append_images", [])
width, height = im.size width, height = im.size
sizes = filter( for size in sorted(set(sizes)):
lambda x: False if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256:
if (x[0] > width or x[1] > height or x[0] > 256 or x[1] > 256) continue
else True,
sizes, for provided_im in provided_ims:
) if provided_im.size != size:
sizes = list(sizes) continue
fp.write(struct.pack("<H", len(sizes))) # idCount(2) frames.append(provided_im)
offset = fp.tell() + len(sizes) * 16 if bmp:
bmp = im.encoderinfo.get("bitmap_format") == "bmp" bits = BmpImagePlugin.SAVE[provided_im.mode][1]
provided_images = {im.size: im for im in im.encoderinfo.get("append_images", [])} bits_used = [bits]
for size in sizes: for other_im in provided_ims:
width, height = size if other_im.size != size:
continue
bits = BmpImagePlugin.SAVE[other_im.mode][1]
if bits not in bits_used:
# Another image has been supplied for this size
# with a different bit depth
frames.append(other_im)
bits_used.append(bits)
break
else:
# TODO: invent a more convenient method for proportional scalings
frame = provided_im.copy()
frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
frames.append(frame)
fp.write(o16(len(frames))) # idCount(2)
offset = fp.tell() + len(frames) * 16
for frame in frames:
width, height = frame.size
# 0 means 256 # 0 means 256
fp.write(struct.pack("B", width if width < 256 else 0)) # bWidth(1) fp.write(o8(width if width < 256 else 0)) # bWidth(1)
fp.write(struct.pack("B", height if height < 256 else 0)) # bHeight(1) fp.write(o8(height if height < 256 else 0)) # bHeight(1)
fp.write(b"\0") # bColorCount(1)
bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0)
fp.write(o8(colors)) # bColorCount(1)
fp.write(b"\0") # bReserved(1) fp.write(b"\0") # bReserved(1)
fp.write(b"\0\0") # wPlanes(2) fp.write(b"\0\0") # wPlanes(2)
fp.write(o16(bits)) # wBitCount(2)
tmp = provided_images.get(size)
if not tmp:
# TODO: invent a more convenient method for proportional scalings
tmp = im.copy()
tmp.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
bits = BmpImagePlugin.SAVE[tmp.mode][1] if bmp else 32
fp.write(struct.pack("<H", bits)) # wBitCount(2)
image_io = BytesIO() image_io = BytesIO()
if bmp: if bmp:
tmp.save(image_io, "dib") frame.save(image_io, "dib")
if bits != 32: if bits != 32:
and_mask = Image.new("1", tmp.size) and_mask = Image.new("1", size)
ImageFile._save( ImageFile._save(
and_mask, image_io, [("raw", (0, 0) + tmp.size, 0, ("1", 0, -1))] and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))]
) )
else: else:
tmp.save(image_io, "png") frame.save(image_io, "png")
image_io.seek(0) image_io.seek(0)
image_bytes = image_io.read() image_bytes = image_io.read()
if bmp: if bmp:
image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:] image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
bytes_len = len(image_bytes) bytes_len = len(image_bytes)
fp.write(struct.pack("<I", bytes_len)) # dwBytesInRes(4) fp.write(o32(bytes_len)) # dwBytesInRes(4)
fp.write(struct.pack("<I", offset)) # dwImageOffset(4) fp.write(o32(offset)) # dwImageOffset(4)
current = fp.tell() current = fp.tell()
fp.seek(offset) fp.seek(offset)
fp.write(image_bytes) fp.write(image_bytes)
@ -304,7 +320,7 @@ class IcoImageFile(ImageFile.ImageFile):
self._size = value self._size = value
def load(self): def load(self):
if self.im and self.im.size == self.size: if self.im is not None and self.im.size == self.size:
# Already loaded # Already loaded
return Image.Image.load(self) return Image.Image.load(self)
im = self.ico.getimage(self.size) im = self.ico.getimage(self.size)

View File

@ -100,7 +100,7 @@ for i in range(2, 33):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Read IM directory # Read IM directory
split = re.compile(br"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
def number(s): def number(s):

View File

@ -49,7 +49,7 @@ except ImportError:
# PILLOW_VERSION was removed in Pillow 9.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0.
# Use __version__ instead. # Use __version__ instead.
from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
from ._binary import i32le from ._binary import i32le, o32be, o32le
from ._util import deferred_error, isPath from ._util import deferred_error, isPath
@ -847,7 +847,7 @@ class Image:
:returns: An image access object. :returns: An image access object.
:rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess` :rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess`
""" """
if self.im and self.palette and self.palette.dirty: if self.im is not None and self.palette and self.palette.dirty:
# realize palette # realize palette
mode, arr = self.palette.getdata() mode, arr = self.palette.getdata()
self.im.putpalette(mode, arr) self.im.putpalette(mode, arr)
@ -864,7 +864,7 @@ class Image:
self.palette.mode = palette_mode self.palette.mode = palette_mode
self.palette.palette = self.im.getpalette(palette_mode, palette_mode) self.palette.palette = self.im.getpalette(palette_mode, palette_mode)
if self.im: if self.im is not None:
if cffi and USE_CFFI_ACCESS: if cffi and USE_CFFI_ACCESS:
if self.pyaccess: if self.pyaccess:
return self.pyaccess return self.pyaccess
@ -975,7 +975,9 @@ class Image:
delete_trns = False delete_trns = False
# transparency handling # transparency handling
if has_transparency: if has_transparency:
if self.mode in ("1", "L", "I", "RGB") and mode == "RGBA": if (self.mode in ("1", "L", "I") and mode in ("LA", "RGBA")) or (
self.mode == "RGB" and mode == "RGBA"
):
# Use transparent conversion to promote from transparent # Use transparent conversion to promote from transparent
# color to an alpha channel. # color to an alpha channel.
new_im = self._new( new_im = self._new(
@ -1416,6 +1418,7 @@ class Image:
"".join(self.info["Raw profile type exif"].split("\n")[3:]) "".join(self.info["Raw profile type exif"].split("\n")[3:])
) )
elif hasattr(self, "tag_v2"): elif hasattr(self, "tag_v2"):
self._exif.bigtiff = self.tag_v2._bigtiff
self._exif.endian = self.tag_v2._endian self._exif.endian = self.tag_v2._endian
self._exif.load_from_fp(self.fp, self.tag_v2._offset) self._exif.load_from_fp(self.fp, self.tag_v2._offset)
if exif_info is not None: if exif_info is not None:
@ -1492,11 +1495,12 @@ class Image:
def histogram(self, mask=None, extrema=None): def histogram(self, mask=None, extrema=None):
""" """
Returns a histogram for the image. The histogram is returned as Returns a histogram for the image. The histogram is returned as a
a list of pixel counts, one for each pixel value in the source list of pixel counts, one for each pixel value in the source
image. If the image has more than one band, the histograms for image. Counts are grouped into 256 bins for each band, even if
all bands are concatenated (for example, the histogram for an the image has more than 8 bits per band. If the image has more
"RGB" image contains 768 values). than one band, the histograms for all bands are concatenated (for
example, the histogram for an "RGB" image contains 768 values).
A bilevel image (mode "1") is treated as a greyscale ("L") image A bilevel image (mode "1") is treated as a greyscale ("L") image
by this method. by this method.
@ -1564,8 +1568,8 @@ class Image:
also use color strings as supported by the ImageColor module. also use color strings as supported by the ImageColor module.
If a mask is given, this method updates only the regions If a mask is given, this method updates only the regions
indicated by the mask. You can use either "1", "L" or "RGBA" indicated by the mask. You can use either "1", "L", "LA", "RGBA"
images (in the latter case, the alpha band is used as mask). or "RGBa" images (if present, the alpha band is used as mask).
Where the mask is 255, the given image is copied as is. Where Where the mask is 255, the given image is copied as is. Where
the mask is 0, the current value is preserved. Intermediate the mask is 0, the current value is preserved. Intermediate
values will mix the two images together, including their alpha values will mix the two images together, including their alpha
@ -1613,7 +1617,7 @@ class Image:
elif isImageType(im): elif isImageType(im):
im.load() im.load()
if self.mode != im.mode: if self.mode != im.mode:
if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"): if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"):
# should use an adapter for this! # should use an adapter for this!
im = im.convert(self.mode) im = im.convert(self.mode)
im = im.im im = im.im
@ -1716,6 +1720,8 @@ class Image:
# FIXME: _imaging returns a confusing error message for this case # FIXME: _imaging returns a confusing error message for this case
raise ValueError("point operation not supported for this mode") raise ValueError("point operation not supported for this mode")
if mode != "F":
lut = [round(i) for i in lut]
return self._new(self.im.point(lut, mode)) return self._new(self.im.point(lut, mode))
def putalpha(self, alpha): def putalpha(self, alpha):
@ -2020,6 +2026,7 @@ class Image:
size = tuple(size) size = tuple(size)
self.load()
if box is None: if box is None:
box = (0, 0) + self.size box = (0, 0) + self.size
else: else:
@ -2282,7 +2289,9 @@ class Image:
else: else:
save_handler = SAVE[format.upper()] save_handler = SAVE[format.upper()]
created = False
if open_fp: if open_fp:
created = not os.path.exists(filename)
if params.get("append", False): if params.get("append", False):
# Open also for reading ("+"), because TIFF save_all # Open also for reading ("+"), because TIFF save_all
# writer needs to go back and edit the written data. # writer needs to go back and edit the written data.
@ -2292,10 +2301,17 @@ class Image:
try: try:
save_handler(self, fp, filename) save_handler(self, fp, filename)
finally: except Exception:
# do what we can to clean up
if open_fp: if open_fp:
fp.close() fp.close()
if created:
try:
os.remove(filename)
except PermissionError:
pass
raise
if open_fp:
fp.close()
def seek(self, frame): def seek(self, frame):
""" """
@ -2435,6 +2451,7 @@ class Image:
:returns: None :returns: None
""" """
self.load()
x, y = map(math.floor, size) x, y = map(math.floor, size)
if x >= self.width and y >= self.height: if x >= self.width and y >= self.height:
return return
@ -2779,9 +2796,9 @@ def frombytes(mode, size, data, decoder_name="raw", *args):
In its simplest form, this function takes three arguments In its simplest form, this function takes three arguments
(mode, size, and unpacked pixel data). (mode, size, and unpacked pixel data).
You can also use any pixel decoder supported by PIL. For more You can also use any pixel decoder supported by PIL. For more
information on available decoders, see the section information on available decoders, see the section
:ref:`Writing Your Own File Decoder <file-decoders>`. :ref:`Writing Your Own File Codec <file-codecs>`.
Note that this function decodes pixel data only, not entire images. Note that this function decodes pixel data only, not entire images.
If you have an entire image in a string, wrap it in a If you have an entire image in a string, wrap it in a
@ -3134,7 +3151,7 @@ def alpha_composite(im1, im2):
def blend(im1, im2, alpha): def blend(im1, im2, alpha):
""" """
Creates a new image by interpolating between two input images, using Creates a new image by interpolating between two input images, using
a constant alpha.:: a constant alpha::
out = image1 * (1.0 - alpha) + image2 * alpha out = image1 * (1.0 - alpha) + image2 * alpha
@ -3423,6 +3440,7 @@ atexit.register(core.clear_cache)
class Exif(MutableMapping): class Exif(MutableMapping):
endian = None endian = None
bigtiff = False
def __init__(self): def __init__(self):
self._data = {} self._data = {}
@ -3458,10 +3476,15 @@ class Exif(MutableMapping):
return self._fixup_dict(info) return self._fixup_dict(info)
def _get_head(self): def _get_head(self):
version = b"\x2B" if self.bigtiff else b"\x2A"
if self.endian == "<": if self.endian == "<":
return b"II\x2A\x00\x08\x00\x00\x00" head = b"II" + version + b"\x00" + o32le(8)
else: else:
return b"MM\x00\x2A\x00\x00\x00\x08" head = b"MM\x00" + version + o32be(8)
if self.bigtiff:
head += o32le(8) if self.endian == "<" else o32be(8)
head += b"\x00\x00\x00\x00"
return head
def load(self, data): def load(self, data):
# Extract EXIF information. This is highly experimental, # Extract EXIF information. This is highly experimental,
@ -3475,12 +3498,12 @@ class Exif(MutableMapping):
self._loaded_exif = data self._loaded_exif = data
self._data.clear() self._data.clear()
self._ifds.clear() self._ifds.clear()
if data and data.startswith(b"Exif\x00\x00"):
data = data[6:]
if not data: if not data:
self._info = None self._info = None
return return
if data.startswith(b"Exif\x00\x00"):
data = data[6:]
self.fp = io.BytesIO(data) self.fp = io.BytesIO(data)
self.head = self.fp.read(8) self.head = self.fp.read(8)
# process dictionary # process dictionary

View File

@ -223,15 +223,15 @@ class ImageFile(Image.Image):
) )
] ]
for decoder_name, extents, offset, args in self.tile: for decoder_name, extents, offset, args in self.tile:
seek(offset)
decoder = Image._getdecoder( decoder = Image._getdecoder(
self.mode, decoder_name, args, self.decoderconfig self.mode, decoder_name, args, self.decoderconfig
) )
try: try:
seek(offset)
decoder.setimage(self.im, extents) decoder.setimage(self.im, extents)
if decoder.pulls_fd: if decoder.pulls_fd:
decoder.setfd(self.fp) decoder.setfd(self.fp)
status, err_code = decoder.decode(b"") err_code = decoder.decode(b"")[1]
else: else:
b = prefix b = prefix
while True: while True:
@ -499,40 +499,33 @@ def _save(im, fp, tile, bufsize=0):
try: try:
fh = fp.fileno() fh = fp.fileno()
fp.flush() fp.flush()
except (AttributeError, io.UnsupportedOperation) as exc: exc = None
# compress to Python file-compatible object except (AttributeError, io.UnsupportedOperation) as e:
for e, b, o, a in tile: exc = e
e = Image._getencoder(im.mode, e, a, im.encoderconfig) for e, b, o, a in tile:
if o > 0: if o > 0:
fp.seek(o) fp.seek(o)
e.setimage(im.im, b) encoder = Image._getencoder(im.mode, e, a, im.encoderconfig)
if e.pushes_fd: try:
e.setfd(fp) encoder.setimage(im.im, b)
l, s = e.encode_to_pyfd() if encoder.pushes_fd:
encoder.setfd(fp)
l, s = encoder.encode_to_pyfd()
else: else:
while True: if exc:
l, s, d = e.encode(bufsize) # compress to Python file-compatible object
fp.write(d) while True:
if s: l, s, d = encoder.encode(bufsize)
break fp.write(d)
if s:
break
else:
# slight speedup: compress to real file object
s = encoder.encode_to_file(fh, bufsize)
if s < 0: if s < 0:
raise OSError(f"encoder error {s} when writing image file") from exc raise OSError(f"encoder error {s} when writing image file") from exc
e.cleanup() finally:
else: encoder.cleanup()
# slight speedup: compress to real file object
for e, b, o, a in tile:
e = Image._getencoder(im.mode, e, a, im.encoderconfig)
if o > 0:
fp.seek(o)
e.setimage(im.im, b)
if e.pushes_fd:
e.setfd(fp)
l, s = e.encode_to_pyfd()
else:
s = e.encode_to_file(fh, bufsize)
if s < 0:
raise OSError(f"encoder error {s} when writing image file")
e.cleanup()
if hasattr(fp, "flush"): if hasattr(fp, "flush"):
fp.flush() fp.flush()
@ -671,7 +664,7 @@ class PyDecoder(PyCodec):
:param buffer: A bytes object with the data to be decoded. :param buffer: A bytes object with the data to be decoded.
:returns: A tuple of ``(bytes consumed, errcode)``. :returns: A tuple of ``(bytes consumed, errcode)``.
If finished with decoding return 0 for the bytes consumed. If finished with decoding return -1 for the bytes consumed.
Err codes are from :data:`.ImageFile.ERRORS`. Err codes are from :data:`.ImageFile.ERRORS`.
""" """
raise NotImplementedError() raise NotImplementedError()
@ -725,6 +718,9 @@ class PyEncoder(PyCodec):
def encode_to_pyfd(self): def encode_to_pyfd(self):
""" """
If ``pushes_fd`` is ``True``, then this method will be used,
and ``encode()`` will only be called once.
:returns: A tuple of ``(bytes consumed, errcode)``. :returns: A tuple of ``(bytes consumed, errcode)``.
Err codes are from :data:`.ImageFile.ERRORS`. Err codes are from :data:`.ImageFile.ERRORS`.
""" """

View File

@ -30,14 +30,18 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
if sys.platform == "darwin": if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp(".png") fh, filepath = tempfile.mkstemp(".png")
os.close(fh) os.close(fh)
subprocess.call(["screencapture", "-x", filepath]) args = ["screencapture"]
if bbox:
left, top, right, bottom = bbox
args += ["-R", f"{left},{right},{right-left},{bottom-top}"]
subprocess.call(args + ["-x", filepath])
im = Image.open(filepath) im = Image.open(filepath)
im.load() im.load()
os.unlink(filepath) os.unlink(filepath)
if bbox: if bbox:
im_cropped = im.crop(bbox) im_resized = im.resize((right - left, bottom - top))
im.close() im.close()
return im_cropped return im_resized
return im return im
elif sys.platform == "win32": elif sys.platform == "win32":
offset, size, data = Image.core.grabscreen_win32( offset, size, data = Image.core.grabscreen_win32(

View File

@ -525,7 +525,7 @@ def invert(image):
lut = [] lut = []
for i in range(256): for i in range(256):
lut.append(255 - i) lut.append(255 - i)
return _lut(image, lut) return image.point(lut) if image.mode == "1" else _lut(image, lut)
def mirror(image): def mirror(image):

View File

@ -32,8 +32,6 @@ class ImagePalette:
an array or a list of ints between 0-255. The list must consist of an array or a list of ints between 0-255. The list must consist of
all channels for one color followed by the next color (e.g. RGBRGBRGB). all channels for one color followed by the next color (e.g. RGBRGBRGB).
Defaults to an empty palette. Defaults to an empty palette.
:param size: An optional palette size. If given, an error is raised
if ``palette`` is not of equal length.
""" """
def __init__(self, mode="RGB", palette=None, size=0): def __init__(self, mode="RGB", palette=None, size=0):

View File

@ -270,8 +270,9 @@ class DisplayViewer(UnixViewer):
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
args = ["display"] args = ["display"]
if "title" in options and options["title"] is not None: title = options.get("title")
args += ["-title", options["title"]] if title:
args += ["-title", title]
args.append(path) args.append(path)
subprocess.Popen(args) subprocess.Popen(args)
@ -368,8 +369,9 @@ class XVViewer(UnixViewer):
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
args = ["xv"] args = ["xv"]
if "title" in options: title = options.get("title")
args += ["-name", options["title"]] if title:
args += ["-name", title]
args.append(path) args.append(path)
subprocess.Popen(args) subprocess.Popen(args)

View File

@ -91,7 +91,7 @@ class Stat:
for i in range(0, len(self.h), 256): for i in range(0, len(self.h), 256):
sum2 = 0.0 sum2 = 0.0
for j in range(256): for j in range(256):
sum2 += (j ** 2) * float(self.h[i + j]) sum2 += (j**2) * float(self.h[i + j])
v.append(sum2) v.append(sum2)
return v return v

View File

@ -26,6 +26,7 @@
# #
import tkinter import tkinter
import warnings
from io import BytesIO from io import BytesIO
from . import Image from . import Image
@ -58,6 +59,33 @@ def _get_image_from_kw(kw):
return Image.open(source) return Image.open(source)
def _pyimagingtkcall(command, photo, id):
tk = photo.tk
try:
tk.call(command, photo, id)
except tkinter.TclError:
# activate Tkinter hook
# may raise an error if it cannot attach to Tkinter
from . import _imagingtk
try:
if hasattr(tk, "interp"):
# Required for PyPy, which always has CFFI installed
from cffi import FFI
ffi = FFI()
# PyPy is using an FFI CDATA element
# (Pdb) self.tk.interp
# <cdata 'Tcl_Interp *' 0x3061b50>
_imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1)
else:
_imagingtk.tkinit(tk.interpaddr(), 1)
except AttributeError:
_imagingtk.tkinit(id(tk), 0)
tk.call(command, photo, id)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# PhotoImage # PhotoImage
@ -156,11 +184,15 @@ class PhotoImage:
:param im: A PIL image. The size must match the target region. If the :param im: A PIL image. The size must match the target region. If the
mode does not match, the image is converted to the mode of mode does not match, the image is converted to the mode of
the bitmap image. the bitmap image.
:param box: A 4-tuple defining the left, upper, right, and lower pixel
coordinate. See :ref:`coordinate-system`. If None is given
instead of a tuple, all of the image is assumed.
""" """
if box is not None:
warnings.warn(
"The box parameter is deprecated and will be removed in Pillow 10 "
"(2023-07-01).",
DeprecationWarning,
)
# convert to blittable # convert to blittable
im.load() im.load()
image = im.im image = im.im
@ -170,33 +202,7 @@ class PhotoImage:
block = image.new_block(self.__mode, im.size) block = image.new_block(self.__mode, im.size)
image.convert2(block, image) # convert directly between buffers image.convert2(block, image) # convert directly between buffers
tk = self.__photo.tk _pyimagingtkcall("PyImagingPhoto", self.__photo, block.id)
try:
tk.call("PyImagingPhoto", self.__photo, block.id)
except tkinter.TclError:
# activate Tkinter hook
try:
from . import _imagingtk
try:
if hasattr(tk, "interp"):
# Required for PyPy, which always has CFFI installed
from cffi import FFI
ffi = FFI()
# PyPy is using an FFI CDATA element
# (Pdb) self.tk.interp
# <cdata 'Tcl_Interp *' 0x3061b50>
_imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1)
else:
_imagingtk.tkinit(tk.interpaddr(), 1)
except AttributeError:
_imagingtk.tkinit(id(tk), 0)
tk.call("PyImagingPhoto", self.__photo, block.id)
except (ImportError, AttributeError, tkinter.TclError):
raise # configuration problem; cannot attach to Tkinter
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -276,7 +282,7 @@ def getimage(photo):
im = Image.new("RGBA", (photo.width(), photo.height())) im = Image.new("RGBA", (photo.width(), photo.height()))
block = im.im block = im.im
photo.tk.call("PyImagingPhotoGet", photo, block.id) _pyimagingtkcall("PyImagingPhotoGet", photo, block.id)
return im return im

View File

@ -22,7 +22,7 @@ from . import Image, ImageFile
# #
# -------------------------------------------------------------------- # --------------------------------------------------------------------
field = re.compile(br"([a-z]*) ([^ \r\n]*)") field = re.compile(rb"([a-z]*) ([^ \r\n]*)")
## ##

View File

@ -132,7 +132,7 @@ def _res_to_dpi(num, denom, exp):
calculated as (num / denom) * 10^exp and stored in dots per meter, calculated as (num / denom) * 10^exp and stored in dots per meter,
to floating-point dots per inch.""" to floating-point dots per inch."""
if denom != 0: if denom != 0:
return (254 * num * (10 ** exp)) / (10000 * denom) return (254 * num * (10**exp)) / (10000 * denom)
def _parse_jp2_header(fp): def _parse_jp2_header(fp):
@ -290,14 +290,14 @@ def _accept(prefix):
def _save(im, fp, filename): def _save(im, fp, filename):
if filename.endswith(".j2k"): # Get the keyword arguments
info = im.encoderinfo
if filename.endswith(".j2k") or info.get("no_jp2", False):
kind = "j2k" kind = "j2k"
else: else:
kind = "jp2" kind = "jp2"
# Get the keyword arguments
info = im.encoderinfo
offset = info.get("offset", None) offset = info.get("offset", None)
tile_offset = info.get("tile_offset", None) tile_offset = info.get("tile_offset", None)
tile_size = info.get("tile_size", None) tile_size = info.get("tile_size", None)
@ -320,6 +320,7 @@ def _save(im, fp, filename):
irreversible = info.get("irreversible", False) irreversible = info.get("irreversible", False)
progression = info.get("progression", "LRCP") progression = info.get("progression", "LRCP")
cinema_mode = info.get("cinema_mode", "no") cinema_mode = info.get("cinema_mode", "no")
mct = info.get("mct", 0)
fd = -1 fd = -1
if hasattr(fp, "fileno"): if hasattr(fp, "fileno"):
@ -340,6 +341,7 @@ def _save(im, fp, filename):
irreversible, irreversible,
progression, progression,
cinema_mode, cinema_mode,
mct,
fd, fd,
) )

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