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

View File

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

View File

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

27
.github/workflows/stale.yml vendored Normal file
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,
debian-10-buster-x86,
debian-11-bullseye-x86,
fedora-34-amd64,
fedora-35-amd64,
gentoo,
ubuntu-18.04-bionic-amd64,
@ -41,7 +40,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Build system information
run: python3 .github/workflows/system-info.py

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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",
"rgb24largepal.bmp",
"pal8os2sp.bmp",
"pal8rletrns.bmp",
"rgb32bf-xbgr.bmp",
]
for f in get_files("q"):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
def gradient_RGBA(self):
return Image.merge(
@ -145,6 +155,28 @@ class TestImagingPaste:
],
)
def test_image_mask_LA(self):
for mode in ("RGBA", "RGB", "L"):
im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode)
self.assert_9points_paste(
im,
im2,
self.gradient_LA,
[
(128, 191, 255, 191),
(112, 207, 206, 111),
(128, 254, 128, 1),
(208, 208, 239, 239),
(192, 191, 191, 191),
(207, 207, 112, 113),
(255, 255, 255, 255),
(239, 207, 207, 239),
(255, 191, 128, 191),
],
)
def test_image_mask_RGBA(self):
for mode in ("RGBA", "RGB", "L"):
im = Image.new(mode, (200, 200), "white")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16))
self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15))
self.assert_unpack("RGBA", "BGRA;16L", 8, (6, 4, 2, 8), (14, 12, 10, 16))
self.assert_unpack("RGBA", "BGRA;16B", 8, (5, 3, 1, 7), (13, 11, 9, 15))
self.assert_unpack(
"RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.
.. note:: Accessing individual pixels is fairly slow. If you are
looping over all of the pixels in an image, there is likely
a faster way using other parts of the Pillow API.
looping over all of the pixels in an image, there is likely
a faster way using other parts of the Pillow API.
:mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps`
have methods for many standard operations. If you wish to perform
a custom mapping, check out :py:meth:`~PIL.Image.Image.point`.
Example
-------

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

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]
docs =
furo
olefile
sphinx>=2.4
sphinx-copybutton
sphinx-issues>=3.0.1
sphinx-removed-in
sphinx-rtd-theme>=1.0
sphinxext-opengraph
tests =
check-manifest

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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