Merge branch 'main' into gif

This commit is contained in:
Andrew Murray 2022-10-12 22:01:35 +11:00 committed by GitHub
commit 5f8938cb8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
101 changed files with 1994 additions and 1604 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\gs9561w32.exe /S
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH%
- ..\pillow-depends\gs1000w32.exe /S
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\

17
.github/renovate.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
],
"labels": [
"Dependency"
],
"packageRules": [
{
"groupName": "github-actions",
"matchManagers": ["github-actions"],
"separateMajorMinor": "false"
}
],
"schedule": ["on the 3rd day of the month"]
}

View File

@ -14,6 +14,10 @@ on:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Fuzzing:
runs-on: ubuntu-latest

View File

@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
@ -16,7 +20,7 @@ jobs:
- uses: actions/checkout@v3
- name: pre-commit cache
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.cache/pre-commit
key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }}
@ -24,7 +28,7 @@ jobs:
lint-pre-commit-
- name: Set up Python
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: "3.10"
cache: pip

View File

@ -10,6 +10,10 @@ on:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
update_release_draft:
permissions:

View File

@ -8,6 +8,10 @@ on:
permissions:
issues: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
stale:
if: github.repository_owner == 'python-pillow'
@ -16,7 +20,7 @@ jobs:
steps:
- name: "Check issues"
uses: actions/stale@v5
uses: actions/stale@v6
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action"

View File

@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: windows-latest
@ -44,7 +48,7 @@ jobs:
qt5-devel-tools subversion xorg-server-extra zlib-devel
- name: Add Lapack to PATH
uses: egor-tensin/cleanup-path@v1
uses: egor-tensin/cleanup-path@v2
with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'

View File

@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
@ -79,7 +83,7 @@ jobs:
MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v3
with:
flags: GHA_Docker
name: ${{ matrix.docker }}

View File

@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: windows-latest
@ -73,11 +77,11 @@ jobs:
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
- name: Upload coverage
run: |
python3 -m pip install codecov
bash <(curl -s https://codecov.io/bash) -F GHA_Windows
env:
CODECOV_NAME: ${{ matrix.name }}
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: GHA_Windows
name: ${{ matrix.name }}
success:
permissions:

View File

@ -16,6 +16,10 @@ on:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:

View File

@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: windows-latest
@ -36,7 +40,7 @@ jobs:
# sets env: pythonLocation
- name: Set up Python
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}
@ -55,8 +59,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\gs9561w32.exe /S
echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH
winbuild\depends\gs1000w32.exe /S
echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH
xcopy /S /Y winbuild\depends\test_images\* Tests\images\
@ -66,7 +70,7 @@ jobs:
- name: Cache build
id: build-cache
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: winbuild\build
key:
@ -171,7 +175,7 @@ jobs:
shell: pwsh
- name: Upload coverage
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: GHA_Windows

View File

@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
@ -30,11 +34,6 @@ jobs:
REVERSE: "--reverse"
- python-version: "3.8"
PYTHONOPTIMIZE: 2
# Include new variables for Codecov
- os: ubuntu-latest
codecov-flag: GHA_Ubuntu
- os: macos-latest
codecov-flag: GHA_macOS
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
@ -43,7 +42,7 @@ jobs:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: pip
@ -99,7 +98,6 @@ jobs:
- name: Docs
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
run: |
python3 -m pip install furo sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph
make doccheck
- name: After success
@ -107,9 +105,11 @@ jobs:
.ci/after_success.sh
- name: Upload coverage
run: bash <(curl -s https://codecov.io/bash) -F ${{ matrix.codecov-flag }}
env:
CODECOV_NAME: ${{ matrix.os }} Python ${{ matrix.python-version }}
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
success:
permissions:

View File

@ -1,4 +1,5 @@
name: Tidelift Align
on:
schedule:
- cron: "30 2 * * *" # daily at 02:30 UTC
@ -15,6 +16,10 @@ on:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
if: github.repository_owner == 'python-pillow'

View File

@ -40,6 +40,7 @@ repos:
rev: v4.3.0
hooks:
- id: check-merge-conflict
- id: check-json
- id: check-yaml
- repo: https://github.com/sphinx-contrib/sphinx-lint

View File

@ -5,6 +5,48 @@ Changelog (Pillow)
9.3.0 (unreleased)
------------------
- Round box position to integer when pasting embedded color #6517
[radarhere, nulano]
- Removed EXIF prefix when saving WebP #6582
[radarhere]
- Pad IM palette to 768 bytes when saving #6579
[radarhere]
- Added DDS BC6 reading #6449
[ShadelessFox, REDxEYE, radarhere]
- Added support for opening WhiteIsZero 16-bit integer TIFF images #6642
[JayWiz, radarhere]
- Raise an error when allocating translucent color to RGB palette #6654
[jsbueno, radarhere]
- Added reading of TIFF child images #6569
[radarhere]
- Improved ImageOps palette handling #6596
[PososikTeam, radarhere]
- Defer parsing of palette into colors #6567
[radarhere]
- Apply transparency to P images in ImageTk.PhotoImage #6559
[radarhere]
- Use rounding in ImageOps contain() and pad() #6522
[bibinhashley, radarhere]
- Fixed GIF remapping to palette with duplicate entries #6548
[radarhere]
- Allow remap_palette() to return an image with less than 256 palette entries #6543
[radarhere]
- Corrected BMP and TGA palette size when saving #6500
[radarhere]
- Do not call load() before draft() in Image.thumbnail #6539
[radarhere]

View File

@ -25,6 +25,7 @@ exclude .coveragerc
exclude .editorconfig
exclude .readthedocs.yml
exclude codecov.yml
exclude renovate.json
global-exclude .git*
global-exclude *.pyc
global-exclude *.so

View File

@ -17,11 +17,12 @@ coverage:
.PHONY: doc
doc:
python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
$(MAKE) -C docs html
.PHONY: doccheck
doccheck:
$(MAKE) -C docs html
$(MAKE) doc
# Don't make our tests rely on the links in the docs being up every single build.
# We don't control them. But do check, and update them to the target of their redirects.
$(MAKE) -C docs linkcheck || true

View File

@ -74,6 +74,9 @@ As of 2019, Pillow development is
<a href="https://pypi.org/project/Pillow/"><img
alt="Number of PyPI downloads"
src="https://img.shields.io/pypi/dm/pillow.svg"></a>
<a href="https://bestpractices.coreinfrastructure.org/projects/6331"><img
alt="OpenSSF Best Practices"
src="https://bestpractices.coreinfrastructure.org/projects/6331/badge"></a>
</td>
</tr>
<tr>

BIN
Tests/images/bc6h.dds Normal file

Binary file not shown.

BIN
Tests/images/bc6h.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
Tests/images/bc6h_sf.dds Normal file

Binary file not shown.

BIN
Tests/images/bc6h_sf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

BIN
Tests/images/child_ifd.tiff Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

View File

@ -1,19 +1,18 @@
import PIL
import PIL.Image
from PIL import Image
def test_sanity():
# Make sure we have the binary extension
PIL.Image.core.new("L", (100, 100))
Image.core.new("L", (100, 100))
# Create an image and do stuff with it.
im = PIL.Image.new("1", (100, 100))
im = Image.new("1", (100, 100))
assert (im.mode, im.size) == ("1", (100, 100))
assert len(im.tobytes()) == 1300
# Create images in all remaining major modes.
PIL.Image.new("L", (100, 100))
PIL.Image.new("P", (100, 100))
PIL.Image.new("RGB", (100, 100))
PIL.Image.new("I", (100, 100))
PIL.Image.new("F", (100, 100))
Image.new("L", (100, 100))
Image.new("P", (100, 100))
Image.new("RGB", (100, 100))
Image.new("I", (100, 100))
Image.new("F", (100, 100))

View File

@ -70,14 +70,14 @@ def test_libimagequant_version():
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant"))
def test_check_modules():
for feature in features.modules:
assert features.check_module(feature) in [True, False]
@pytest.mark.parametrize("feature", features.modules)
def test_check_modules(feature):
assert features.check_module(feature) in [True, False]
def test_check_codecs():
for feature in features.codecs:
assert features.check_codec(feature) in [True, False]
@pytest.mark.parametrize("feature", features.codecs)
def test_check_codecs(feature):
assert features.check_codec(feature) in [True, False]
def test_check_warns_on_nonexistent():

View File

@ -39,13 +39,12 @@ def test_apng_basic():
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
def test_apng_fdat():
with Image.open("Tests/images/apng/split_fdat.png") as im:
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/split_fdat_zero_chunk.png") as im:
@pytest.mark.parametrize(
"filename",
("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"),
)
def test_apng_fdat(filename):
with Image.open(filename) as im:
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)

View File

@ -58,6 +58,18 @@ def test_save_to_bytes():
assert reloaded.format == "BMP"
def test_small_palette(tmp_path):
im = Image.new("P", (1, 1))
colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
im.putpalette(colors)
out = str(tmp_path / "temp.bmp")
im.save(out)
with Image.open(out) as reloaded:
assert reloaded.getpalette() == colors
def test_save_too_large(tmp_path):
outfile = str(tmp_path / "temp.bmp")
with Image.new("RGB", (1, 1)) as im:

View File

@ -16,6 +16,8 @@ TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
TEST_FILE_BC5S = "Tests/images/bc5s.dds"
TEST_FILE_BC6H = "Tests/images/bc6h.dds"
TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds"
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds"
TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds"
@ -114,6 +116,20 @@ def test_dx10_bc5(image_path, expected_path):
assert_image_equal_tofile(im, expected_path.replace(".dds", ".png"))
@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS))
def test_dx10_bc6h(image_path):
"""Check DX10 BC6H/BC6HS images can be opened"""
with Image.open(image_path) as im:
im.load()
assert im.format == "DDS"
assert im.mode == "RGB"
assert im.size == (128, 128)
assert_image_equal_tofile(im, image_path.replace(".dds", ".png"))
def test_dx10_bc7():
"""Check DX10 images can be opened"""

View File

@ -124,14 +124,6 @@ def test_file_object(tmp_path):
image1.save(fh, "EPS")
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_iobase_object(tmp_path):
# issue 479
with Image.open(FILE1) as image1:
with open(str(tmp_path / "temp_iobase.eps"), "wb") as fh:
image1.save(fh, "EPS")
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_bytesio_object():
with open(FILE1, "rb") as f:
@ -203,25 +195,23 @@ def test_render_scale2():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_resize():
files = [FILE1, FILE2, "Tests/images/illu10_preview.eps"]
for fn in files:
with Image.open(fn) as im:
new_size = (100, 100)
im = im.resize(new_size)
assert im.size == new_size
@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps"))
def test_resize(filename):
with Image.open(filename) as im:
new_size = (100, 100)
im = im.resize(new_size)
assert im.size == new_size
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_thumbnail():
@pytest.mark.parametrize("filename", (FILE1, FILE2))
def test_thumbnail(filename):
# Issue #619
# Arrange
files = [FILE1, FILE2]
for fn in files:
with Image.open(FILE1) as im:
new_size = (100, 100)
im.thumbnail(new_size)
assert max(im.size) == max(new_size)
with Image.open(filename) as im:
new_size = (100, 100)
im.thumbnail(new_size)
assert max(im.size) == max(new_size)
def test_read_binary_preview():
@ -266,20 +256,19 @@ def test_readline(tmp_path):
_test_readline_file_psfile(s, ending)
def test_open_eps():
# https://github.com/python-pillow/Pillow/issues/1104
# Arrange
FILES = [
@pytest.mark.parametrize(
"filename",
(
"Tests/images/illu10_no_preview.eps",
"Tests/images/illu10_preview.eps",
"Tests/images/illuCS6_no_preview.eps",
"Tests/images/illuCS6_preview.eps",
]
# Act / Assert
for filename in FILES:
with Image.open(filename) as img:
assert img.mode == "RGB"
),
)
def test_open_eps(filename):
# https://github.com/python-pillow/Pillow/issues/1104
with Image.open(filename) as img:
assert img.mode == "RGB"
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")

View File

@ -4,7 +4,7 @@ import pytest
from PIL import FliImagePlugin, Image
from .helper import assert_image_equal_tofile, is_pypy
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
# created as an export of a palette image from Gimp2.6
# save as...-> hopper.fli, default options.
@ -79,6 +79,12 @@ def test_invalid_file():
FliImagePlugin.FliImageFile(invalid_file)
def test_palette_chunk_second():
with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im:
with Image.open(static_test_file) as expected:
assert_image_equal(im.convert("RGB"), expected.convert("RGB"))
def test_n_frames():
with Image.open(static_test_file) as im:
assert im.n_frames == 1

View File

@ -99,17 +99,24 @@ def test_palette_not_needed_for_second_frame():
def test_strategy():
with Image.open("Tests/images/iss634.gif") as im:
expected_rgb_always = im.convert("RGB")
with Image.open("Tests/images/chi.gif") as im:
expected_zero = im.convert("RGB")
expected_rgb_always_rgba = im.convert("RGBA")
im.seek(1)
expected_one = im.convert("RGB")
expected_different = im.convert("RGB")
try:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
with Image.open("Tests/images/chi.gif") as im:
with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "RGB"
assert_image_equal(im, expected_zero)
assert_image_equal(im, expected_rgb_always)
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "RGBA"
assert_image_equal(im, expected_rgb_always_rgba)
GifImagePlugin.LOADING_STRATEGY = (
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
@ -120,7 +127,7 @@ def test_strategy():
im.seek(1)
assert im.mode == "P"
assert_image_equal(im.convert("RGB"), expected_one)
assert_image_equal(im.convert("RGB"), expected_different)
# Change to RGB mode when a frame has an individual palette
with Image.open("Tests/images/iss634.gif") as im:
@ -808,24 +815,24 @@ def test_identical_frames(tmp_path):
assert reread.info["duration"] == 4500
def test_identical_frames_to_single_frame(tmp_path):
for duration in ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500):
out = str(tmp_path / "temp.gif")
im_list = [
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"),
]
@pytest.mark.parametrize(
"duration", ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500)
)
def test_identical_frames_to_single_frame(duration, tmp_path):
out = str(tmp_path / "temp.gif")
im_list = [
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"),
]
im_list[0].save(
out, save_all=True, append_images=im_list[1:], duration=duration
)
with Image.open(out) as reread:
# Assert that all frames were combined
assert reread.n_frames == 1
im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)
with Image.open(out) as reread:
# Assert that all frames were combined
assert reread.n_frames == 1
# Assert that the new duration is the total of the identical frames
assert reread.info["duration"] == 8500
# Assert that the new duration is the total of the identical frames
assert reread.info["duration"] == 8500
def test_number_of_loops(tmp_path):
@ -1102,6 +1109,19 @@ def test_palette_save_P(tmp_path):
assert_image_equal(reloaded, im)
def test_palette_save_duplicate_entries(tmp_path):
im = Image.new("P", (1, 2))
im.putpixel((0, 1), 1)
im.putpalette((0, 0, 0, 0, 0, 0))
out = str(tmp_path / "temp.gif")
im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1])
with Image.open(out) as reloaded:
assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0)
def test_palette_save_all_P(tmp_path):
frames = []
colors = ((255, 0, 0), (0, 255, 0))

View File

@ -86,6 +86,18 @@ def test_roundtrip(mode, tmp_path):
assert_image_equal_tofile(im, out)
def test_small_palette(tmp_path):
im = Image.new("P", (1, 1))
colors = [0, 1, 2]
im.putpalette(colors)
out = str(tmp_path / "temp.im")
im.save(out)
with Image.open(out) as reloaded:
assert reloaded.getpalette() == colors + [0] * 765
def test_save_unsupported_mode(tmp_path):
out = str(tmp_path / "temp.im")
im = hopper("HSV")

19
Tests/test_file_imt.py Normal file
View File

@ -0,0 +1,19 @@
import io
import pytest
from PIL import Image, ImtImagePlugin
from .helper import assert_image_equal_tofile
def test_sanity():
with Image.open("Tests/images/bw_gradient.imt") as im:
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
@pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n"))
def test_invalid_file(data):
with io.BytesIO(data) as fp:
with pytest.raises(SyntaxError):
ImtImagePlugin.ImtImageFile(fp)

View File

@ -150,27 +150,30 @@ class TestFileJpeg:
assert not im1.info.get("icc_profile")
assert im2.info.get("icc_profile")
def test_icc_big(self):
@pytest.mark.parametrize(
"n",
(
0,
1,
3,
4,
5,
65533 - 14, # full JPEG marker block
65533 - 14 + 1, # full block plus one byte
ImageFile.MAXBLOCK, # full buffer block
ImageFile.MAXBLOCK + 1, # full buffer block plus one byte
ImageFile.MAXBLOCK * 4 + 3, # large block
),
)
def test_icc_big(self, n):
# Make sure that the "extra" support handles large blocks
def test(n):
# The ICC APP marker can store 65519 bytes per marker, so
# using a 4-byte test code should allow us to detect out of
# order issues.
icc_profile = (b"Test" * int(n / 4 + 1))[:n]
assert len(icc_profile) == n # sanity
im1 = self.roundtrip(hopper(), icc_profile=icc_profile)
assert im1.info.get("icc_profile") == (icc_profile or None)
test(0)
test(1)
test(3)
test(4)
test(5)
test(65533 - 14) # full JPEG marker block
test(65533 - 14 + 1) # full block plus one byte
test(ImageFile.MAXBLOCK) # full buffer block
test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte
test(ImageFile.MAXBLOCK * 4 + 3) # large block
# The ICC APP marker can store 65519 bytes per marker, so
# using a 4-byte test code should allow us to detect out of
# order issues.
icc_profile = (b"Test" * int(n / 4 + 1))[:n]
assert len(icc_profile) == n # sanity
im1 = self.roundtrip(hopper(), icc_profile=icc_profile)
assert im1.info.get("icc_profile") == (icc_profile or None)
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@ -649,19 +652,19 @@ class TestFileJpeg:
# Assert
assert im.format == "JPEG"
def test_save_correct_modes(self):
@pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr"))
def test_save_correct_modes(self, mode):
out = BytesIO()
for mode in ["1", "L", "RGB", "RGBX", "CMYK", "YCbCr"]:
img = Image.new(mode, (20, 20))
img.save(out, "JPEG")
img = Image.new(mode, (20, 20))
img.save(out, "JPEG")
def test_save_wrong_modes(self):
@pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P"))
def test_save_wrong_modes(self, mode):
# ref https://github.com/python-pillow/Pillow/issues/2005
out = BytesIO()
for mode in ["LA", "La", "RGBA", "RGBa", "P"]:
img = Image.new(mode, (20, 20))
with pytest.raises(OSError):
img.save(out, "JPEG")
img = Image.new(mode, (20, 20))
with pytest.raises(OSError):
img.save(out, "JPEG")
def test_save_tiff_with_dpi(self, tmp_path):
# Arrange

View File

@ -126,14 +126,14 @@ def test_prog_res_rt():
assert_image_equal(im, test_card)
def test_default_num_resolutions():
for num_resolutions in range(2, 6):
d = 1 << (num_resolutions - 1)
im = test_card.resize((d - 1, d - 1))
with pytest.raises(OSError):
roundtrip(im, num_resolutions=num_resolutions)
reloaded = roundtrip(im)
assert_image_equal(im, reloaded)
@pytest.mark.parametrize("num_resolutions", range(2, 6))
def test_default_num_resolutions(num_resolutions):
d = 1 << (num_resolutions - 1)
im = test_card.resize((d - 1, d - 1))
with pytest.raises(OSError):
roundtrip(im, num_resolutions=num_resolutions)
reloaded = roundtrip(im)
assert_image_equal(im, reloaded)
def test_reduce():
@ -266,14 +266,11 @@ def test_rgba():
assert jp2.mode == "RGBA"
def test_16bit_monochrome_has_correct_mode():
with Image.open("Tests/images/16bit.cropped.j2k") as j2k:
j2k.load()
assert j2k.mode == "I;16"
with Image.open("Tests/images/16bit.cropped.jp2") as jp2:
jp2.load()
assert jp2.mode == "I;16"
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
def test_16bit_monochrome_has_correct_mode(ext):
with Image.open("Tests/images/16bit.cropped" + ext) as im:
im.load()
assert im.mode == "I;16"
def test_16bit_monochrome_jp2_like_tiff():

View File

@ -509,20 +509,13 @@ class TestFileLibTiff(LibTiffTestCase):
# colormap/palette tag
assert len(reloaded.tag_v2[320]) == 768
def xtest_bw_compression_w_rgb(self, tmp_path):
"""This test passes, but when running all tests causes a failure due
to output on stderr from the error thrown by libtiff. We need to
capture that but not now"""
@pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
def test_bw_compression_w_rgb(self, compression, tmp_path):
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
with pytest.raises(OSError):
im.save(out, compression="tiff_ccitt")
with pytest.raises(OSError):
im.save(out, compression="group3")
with pytest.raises(OSError):
im.save(out, compression="group4")
im.save(out, compression=compression)
def test_fp_leak(self):
im = Image.open("Tests/images/hopper_g4_500.tif")

View File

@ -63,19 +63,7 @@ def test_p_mode(tmp_path):
roundtrip(tmp_path, mode)
def test_l_oserror(tmp_path):
# Arrange
mode = "L"
# Act / Assert
with pytest.raises(OSError):
helper_save_as_palm(tmp_path, mode)
def test_rgb_oserror(tmp_path):
# Arrange
mode = "RGB"
# Act / Assert
@pytest.mark.parametrize("mode", ("L", "RGB"))
def test_oserror(tmp_path, mode):
with pytest.raises(OSError):
helper_save_as_palm(tmp_path, mode)

View File

@ -39,14 +39,14 @@ def test_invalid_file():
PcxImagePlugin.PcxImageFile(invalid_file)
def test_odd(tmp_path):
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
def test_odd(tmp_path, mode):
# See issue #523, odd sized images should have a stride that's even.
# Not that ImageMagick or GIMP write PCX that way.
# We were not handling properly.
for mode in ("1", "L", "P", "RGB"):
# larger, odd sized images are better here to ensure that
# we handle interrupted scan lines properly.
_roundtrip(tmp_path, hopper(mode).resize((511, 511)))
# larger, odd sized images are better here to ensure that
# we handle interrupted scan lines properly.
_roundtrip(tmp_path, hopper(mode).resize((511, 511)))
def test_odd_read():

View File

@ -37,6 +37,11 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs):
return outfile
@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK"))
def test_save(tmp_path, mode):
helper_save_as_pdf(tmp_path, mode)
@pytest.mark.valgrind_known_error(reason="Temporary skip")
def test_monochrome(tmp_path):
# Arrange
@ -47,38 +52,6 @@ def test_monochrome(tmp_path):
assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000)
def test_greyscale(tmp_path):
# Arrange
mode = "L"
# Act / Assert
helper_save_as_pdf(tmp_path, mode)
def test_rgb(tmp_path):
# Arrange
mode = "RGB"
# Act / Assert
helper_save_as_pdf(tmp_path, mode)
def test_p_mode(tmp_path):
# Arrange
mode = "P"
# Act / Assert
helper_save_as_pdf(tmp_path, mode)
def test_cmyk_mode(tmp_path):
# Arrange
mode = "CMYK"
# Act / Assert
helper_save_as_pdf(tmp_path, mode)
def test_unsupported_mode(tmp_path):
im = hopper("LA")
outfile = str(tmp_path / "temp_LA.pdf")

View File

@ -120,6 +120,18 @@ def test_save(tmp_path):
assert test_im.size == (100, 100)
def test_small_palette(tmp_path):
im = Image.new("P", (1, 1))
colors = [0, 0, 0]
im.putpalette(colors)
out = str(tmp_path / "temp.tga")
im.save(out)
with Image.open(out) as reloaded:
assert reloaded.getpalette() == colors
def test_save_wrong_mode(tmp_path):
im = hopper("PA")
out = str(tmp_path / "temp.tga")

View File

@ -84,6 +84,24 @@ class TestFileTiff:
with Image.open("Tests/images/multipage.tiff") as im:
im.load()
@pytest.mark.parametrize(
"path, sizes",
(
("Tests/images/hopper.tif", ()),
("Tests/images/child_ifd.tiff", (16, 8)),
("Tests/images/child_ifd_jpeg.tiff", (20,)),
),
)
def test_get_child_images(self, path, sizes):
with Image.open(path) as im:
ims = im.get_child_images()
assert len(ims) == len(sizes)
for i, im in enumerate(ims):
w = sizes[i]
expected = Image.new("RGB", (w, w), "#f00")
assert_image_similar(im, expected, 1)
def test_mac_tiff(self):
# Read RGBa images from macOS [@PIL136]
@ -293,14 +311,17 @@ class TestFileTiff:
with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"):
pass
def test_n_frames(self):
for path, n_frames in [
["Tests/images/multipage-lastframe.tif", 1],
["Tests/images/multipage.tiff", 3],
]:
with Image.open(path) as im:
assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1)
@pytest.mark.parametrize(
"path, n_frames",
(
("Tests/images/multipage-lastframe.tif", 1),
("Tests/images/multipage.tiff", 3),
),
)
def test_n_frames(self, path, n_frames):
with Image.open(path) as im:
assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1)
def test_eoferror(self):
with Image.open("Tests/images/multipage-lastframe.tif") as im:
@ -416,12 +437,12 @@ class TestFileTiff:
len_after = len(dict(im.ifd))
assert len_before == len_after + 1
def test_load_byte(self):
for legacy_api in [False, True]:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abc"
ret = ifd.load_byte(data, legacy_api)
assert ret == b"abc"
@pytest.mark.parametrize("legacy_api", (False, True))
def test_load_byte(self, legacy_api):
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abc"
ret = ifd.load_byte(data, legacy_api)
assert ret == b"abc"
def test_load_string(self):
ifd = TiffImagePlugin.ImageFileDirectory_v2()
@ -667,18 +688,15 @@ class TestFileTiff:
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")
@pytest.mark.parametrize("mode", ("P", "PA"))
def test_palette(self, mode, tmp_path):
outfile = str(tmp_path / "temp.tif")
im = hopper(mode)
im.save(outfile)
im = hopper(mode)
im.save(outfile)
with Image.open(outfile) as reloaded:
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
for mode in ["P", "PA"]:
roundtrip(mode)
with Image.open(outfile) as reloaded:
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
def test_tiff_save_all(self):
mp = BytesIO()

View File

@ -55,9 +55,7 @@ def test_write_exif_metadata():
test_buffer.seek(0)
with Image.open(test_buffer) as webp_image:
webp_exif = webp_image.info.get("exif", None)
assert webp_exif
if webp_exif:
assert webp_exif == expected_exif, "WebP EXIF didn't match"
assert webp_exif == expected_exif[6:], "WebP EXIF didn't match"
def test_read_icc_profile():

View File

@ -1,5 +1,7 @@
import os
import pytest
from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile
from .helper import (
@ -59,23 +61,13 @@ def save_font(request, tmp_path, encoding):
return tempname
def _test_sanity(request, tmp_path, encoding):
@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
def test_sanity(request, tmp_path, encoding):
save_font(request, tmp_path, encoding)
def test_sanity_iso8859_1(request, tmp_path):
_test_sanity(request, tmp_path, "iso8859-1")
def test_sanity_iso8859_2(request, tmp_path):
_test_sanity(request, tmp_path, "iso8859-2")
def test_sanity_cp1250(request, tmp_path):
_test_sanity(request, tmp_path, "cp1250")
def _test_draw(request, tmp_path, encoding):
@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
def test_draw(request, tmp_path, encoding):
tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname)
im = Image.new("L", (150, 30), "white")
@ -85,19 +77,8 @@ def _test_draw(request, tmp_path, encoding):
assert_image_similar_tofile(im, charsets[encoding]["image1"], 0)
def test_draw_iso8859_1(request, tmp_path):
_test_draw(request, tmp_path, "iso8859-1")
def test_draw_iso8859_2(request, tmp_path):
_test_draw(request, tmp_path, "iso8859-2")
def test_draw_cp1250(request, tmp_path):
_test_draw(request, tmp_path, "cp1250")
def _test_textsize(request, tmp_path, encoding):
@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
def test_textsize(request, tmp_path, encoding):
tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname)
for i in range(255):
@ -112,15 +93,3 @@ def _test_textsize(request, tmp_path, encoding):
msg = message[: i + 1]
assert font.getlength(msg) == len(msg) * 10
assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20)
def test_textsize_iso8859_1(request, tmp_path):
_test_textsize(request, tmp_path, "iso8859-1")
def test_textsize_iso8859_2(request, tmp_path):
_test_textsize(request, tmp_path, "iso8859-2")
def test_textsize_cp1250(request, tmp_path):
_test_textsize(request, tmp_path, "cp1250")

View File

@ -620,6 +620,7 @@ class TestImage:
im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1
assert len(im_remapped.getpalette()) == 6
# Test unused transparency
im.info["transparency"] = 2

View File

@ -345,13 +345,14 @@ class TestCffi(AccessTest):
@pytest.mark.parametrize("mode", ("P", "PA"))
def test_p_putpixel_rgb_rgba(self, mode):
for color in [(255, 0, 0), (255, 0, 0, 127)]:
for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
im = Image.new(mode, (1, 1))
access = PyAccess.new(im, False)
access.putpixel((0, 0), color)
alpha = color[3] if len(color) == 4 and mode == "PA" else 255
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
if len(color) == 3:
color += (255,)
assert im.convert("RGBA").getpixel((0, 0)) == color
class TestImagePutPixelError(AccessTest):

View File

@ -35,10 +35,13 @@ def test_toarray():
test_with_dtype(numpy.float64)
test_with_dtype(numpy.uint8)
if parse_version(numpy.__version__) >= parse_version("1.23"):
with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
if parse_version(numpy.__version__) >= parse_version("1.23"):
with pytest.raises(OSError):
numpy.array(im_truncated)
else:
with pytest.warns(UserWarning):
numpy.array(im_truncated)
def test_fromarray():

View File

@ -196,11 +196,11 @@ def assert_compare_images(a, b, max_average_diff, max_diff=255):
)
def test_mode_L():
@pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_L(factor):
im = get_image("L")
for factor in remarkable_factors:
compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor)
compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor)
@pytest.mark.parametrize("factor", remarkable_factors)

View File

@ -554,44 +554,48 @@ class TestCoreResampleBox:
# check that the difference at least that much
assert_image_similar(res, im.crop(box), 20, f">>> {size} {box}")
def test_skip_horizontal(self):
@pytest.mark.parametrize(
"flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC)
)
def test_skip_horizontal(self, flt):
# Can skip resize for one dimension
im = hopper()
for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]:
for size, box in [
((40, 50), (0, 0, 40, 90)),
((40, 50), (0, 20, 40, 90)),
((40, 50), (10, 0, 50, 90)),
((40, 50), (10, 20, 50, 90)),
]:
res = im.resize(size, flt, box)
assert res.size == size
# Borders should be slightly different
assert_image_similar(
res,
im.crop(box).resize(size, flt),
0.4,
f">>> {size} {box} {flt}",
)
for size, box in [
((40, 50), (0, 0, 40, 90)),
((40, 50), (0, 20, 40, 90)),
((40, 50), (10, 0, 50, 90)),
((40, 50), (10, 20, 50, 90)),
]:
res = im.resize(size, flt, box)
assert res.size == size
# Borders should be slightly different
assert_image_similar(
res,
im.crop(box).resize(size, flt),
0.4,
f">>> {size} {box} {flt}",
)
def test_skip_vertical(self):
@pytest.mark.parametrize(
"flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC)
)
def test_skip_vertical(self, flt):
# Can skip resize for one dimension
im = hopper()
for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]:
for size, box in [
((40, 50), (0, 0, 90, 50)),
((40, 50), (20, 0, 90, 50)),
((40, 50), (0, 10, 90, 60)),
((40, 50), (20, 10, 90, 60)),
]:
res = im.resize(size, flt, box)
assert res.size == size
# Borders should be slightly different
assert_image_similar(
res,
im.crop(box).resize(size, flt),
0.4,
f">>> {size} {box} {flt}",
)
for size, box in [
((40, 50), (0, 0, 90, 50)),
((40, 50), (20, 0, 90, 50)),
((40, 50), (0, 10, 90, 60)),
((40, 50), (20, 10, 90, 60)),
]:
res = im.resize(size, flt, box)
assert res.size == size
# Borders should be slightly different
assert_image_similar(
res,
im.crop(box).resize(size, flt),
0.4,
f">>> {size} {box} {flt}",
)

View File

@ -1,3 +1,5 @@
import pytest
from PIL import Image, features
from .helper import assert_image_equal, hopper
@ -29,19 +31,12 @@ def test_split():
assert split("YCbCr") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)]
def test_split_merge():
def split_merge(mode):
return Image.merge(mode, hopper(mode).split())
assert_image_equal(hopper("1"), split_merge("1"))
assert_image_equal(hopper("L"), split_merge("L"))
assert_image_equal(hopper("I"), split_merge("I"))
assert_image_equal(hopper("F"), split_merge("F"))
assert_image_equal(hopper("P"), split_merge("P"))
assert_image_equal(hopper("RGB"), split_merge("RGB"))
assert_image_equal(hopper("RGBA"), split_merge("RGBA"))
assert_image_equal(hopper("CMYK"), split_merge("CMYK"))
assert_image_equal(hopper("YCbCr"), split_merge("YCbCr"))
@pytest.mark.parametrize(
"mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr")
)
def test_split_merge(mode):
expected = Image.merge(mode, hopper(mode).split())
assert_image_equal(hopper(mode), expected)
def test_split_open(tmp_path):

View File

@ -64,7 +64,9 @@ def test_mode_mismatch():
ImageDraw.ImageDraw(im, mode="L")
def helper_arc(bbox, start, end):
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4)))
def test_arc(bbox, start, end):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -76,16 +78,6 @@ def helper_arc(bbox, start, end):
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1)
def test_arc1():
helper_arc(BBOX1, 0, 180)
helper_arc(BBOX1, 0.5, 180.4)
def test_arc2():
helper_arc(BBOX2, 0, 180)
helper_arc(BBOX2, 0.5, 180.4)
def test_arc_end_le_start():
# Arrange
im = Image.new("RGB", (W, H))
@ -192,29 +184,21 @@ def test_bitmap():
assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png")
def helper_chord(mode, bbox, start, end):
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
def test_chord(mode, bbox):
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
expected = f"Tests/images/imagedraw_chord_{mode}.png"
# Act
draw.chord(bbox, start, end, fill="red", outline="yellow")
draw.chord(bbox, 0, 180, fill="red", outline="yellow")
# Assert
assert_image_similar_tofile(im, expected, 1)
def test_chord1():
for mode in ["RGB", "L"]:
helper_chord(mode, BBOX1, 0, 180)
def test_chord2():
for mode in ["RGB", "L"]:
helper_chord(mode, BBOX2, 0, 180)
def test_chord_width():
# Arrange
im = Image.new("RGB", (W, H))
@ -263,7 +247,9 @@ def test_chord_too_fat():
assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_too_fat.png")
def helper_ellipse(mode, bbox):
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
def test_ellipse(mode, bbox):
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
@ -276,16 +262,6 @@ def helper_ellipse(mode, bbox):
assert_image_similar_tofile(im, expected, 1)
def test_ellipse1():
for mode in ["RGB", "L"]:
helper_ellipse(mode, BBOX1)
def test_ellipse2():
for mode in ["RGB", "L"]:
helper_ellipse(mode, BBOX2)
def test_ellipse_translucent():
# Arrange
im = Image.new("RGB", (W, H))
@ -405,7 +381,8 @@ def test_ellipse_various_sizes_filled():
)
def helper_line(points):
@pytest.mark.parametrize("points", (POINTS1, POINTS2))
def test_line(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -417,14 +394,6 @@ def helper_line(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
def test_line1():
helper_line(POINTS1)
def test_line2():
helper_line(POINTS2)
def test_shape1():
# Arrange
im = Image.new("RGB", (100, 100), "white")
@ -484,7 +453,9 @@ def test_transform():
assert_image_equal(im, expected)
def helper_pieslice(bbox, start, end):
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2)))
def test_pieslice(bbox, start, end):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -496,16 +467,6 @@ def helper_pieslice(bbox, start, end):
assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1)
def test_pieslice1():
helper_pieslice(BBOX1, -92, 46)
helper_pieslice(BBOX1, -92.2, 46.2)
def test_pieslice2():
helper_pieslice(BBOX2, -92, 46)
helper_pieslice(BBOX2, -92.2, 46.2)
def test_pieslice_width():
# Arrange
im = Image.new("RGB", (W, H))
@ -585,7 +546,8 @@ def test_pieslice_no_spikes():
assert_image_equal(im, im_pre_erase)
def helper_point(points):
@pytest.mark.parametrize("points", (POINTS1, POINTS2))
def test_point(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -597,15 +559,8 @@ def helper_point(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
def test_point1():
helper_point(POINTS1)
def test_point2():
helper_point(POINTS2)
def helper_polygon(points):
@pytest.mark.parametrize("points", (POINTS1, POINTS2))
def test_polygon(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -617,14 +572,6 @@ def helper_polygon(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
def test_polygon1():
helper_polygon(POINTS1)
def test_polygon2():
helper_polygon(POINTS2)
@pytest.mark.parametrize("mode", ("RGB", "L"))
def test_polygon_kite(mode):
# Test drawing lines of different gradients (dx>dy, dy>dx) and
@ -682,7 +629,8 @@ def test_polygon_translucent():
assert_image_equal_tofile(im, expected)
def helper_rectangle(bbox):
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
def test_rectangle(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -694,14 +642,6 @@ def helper_rectangle(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png")
def test_rectangle1():
helper_rectangle(BBOX1)
def test_rectangle2():
helper_rectangle(BBOX2)
def test_big_rectangle():
# Test drawing a rectangle bigger than the image
# Arrange
@ -1503,7 +1443,7 @@ def test_discontiguous_corners_polygon():
assert_image_similar_tofile(img, expected, 1)
def test_polygon():
def test_polygon2():
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red")

View File

@ -52,27 +52,19 @@ def test_sanity():
draw.line(list(range(10)), pen)
def helper_ellipse(mode, bbox):
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
def test_ellipse(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
pen = ImageDraw2.Pen("blue", width=2)
brush = ImageDraw2.Brush("green")
expected = f"Tests/images/imagedraw_ellipse_{mode}.png"
# Act
draw.ellipse(bbox, pen, brush)
# Assert
assert_image_similar_tofile(im, expected, 1)
def test_ellipse1():
helper_ellipse("RGB", BBOX1)
def test_ellipse2():
helper_ellipse("RGB", BBOX2)
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1)
def test_ellipse_edge():
@ -88,7 +80,8 @@ def test_ellipse_edge():
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1)
def helper_line(points):
@pytest.mark.parametrize("points", (POINTS1, POINTS2))
def test_line(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@ -101,14 +94,6 @@ def helper_line(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
def test_line1_pen():
helper_line(POINTS1)
def test_line2_pen():
helper_line(POINTS2)
def test_line_pen_as_brush():
# Arrange
im = Image.new("RGB", (W, H))
@ -124,7 +109,8 @@ def test_line_pen_as_brush():
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
def helper_polygon(points):
@pytest.mark.parametrize("points", (POINTS1, POINTS2))
def test_polygon(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@ -138,15 +124,8 @@ def helper_polygon(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
def test_polygon1():
helper_polygon(POINTS1)
def test_polygon2():
helper_polygon(POINTS2)
def helper_rectangle(bbox):
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
def test_rectangle(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@ -160,14 +139,6 @@ def helper_rectangle(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png")
def test_rectangle1():
helper_rectangle(BBOX1)
def test_rectangle2():
helper_rectangle(BBOX2)
def test_big_rectangle():
# Test drawing a rectangle bigger than the image
# Arrange

View File

@ -1,3 +1,5 @@
import pytest
from PIL import Image, ImageEnhance
from .helper import assert_image_equal, hopper
@ -39,17 +41,17 @@ def _check_alpha(im, original, op, amount):
)
def test_alpha():
@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness"))
def test_alpha(op):
# Issue https://github.com/python-pillow/Pillow/issues/899
# Is alpha preserved through image enhancement?
original = _half_transparent_image()
for op in ["Color", "Brightness", "Contrast", "Sharpness"]:
for amount in [0, 0.5, 1.0]:
_check_alpha(
getattr(ImageEnhance, op)(original).enhance(amount),
original,
op,
amount,
)
for amount in [0, 0.5, 1.0]:
_check_alpha(
getattr(ImageEnhance, op)(original).enhance(amount),
original,
op,
amount,
)

File diff suppressed because it is too large Load Diff

View File

@ -65,14 +65,16 @@ def create_lut():
# create_lut()
def test_lut():
for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"):
lb = ImageMorph.LutBuilder(op_name=op)
assert lb.get_lut() is None
@pytest.mark.parametrize(
"op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge")
)
def test_lut(op):
lb = ImageMorph.LutBuilder(op_name=op)
assert lb.get_lut() is None
lut = lb.build_lut()
with open(f"Tests/images/{op}.lut", "rb") as f:
assert lut == bytearray(f.read())
lut = lb.build_lut()
with open(f"Tests/images/{op}.lut", "rb") as f:
assert lut == bytearray(f.read())
def test_no_operator_loaded():

View File

@ -110,6 +110,16 @@ def test_contain(new_size):
assert new_im.size == (256, 256)
def test_contain_round():
im = Image.new("1", (43, 63), 1)
new_im = ImageOps.contain(im, (5, 7))
assert new_im.width == 5
im = Image.new("1", (63, 43), 1)
new_im = ImageOps.contain(im, (7, 5))
assert new_im.height == 5
def test_pad():
# Same ratio
im = hopper()
@ -130,6 +140,30 @@ def test_pad():
)
def test_pad_round():
im = Image.new("1", (1, 1), 1)
new_im = ImageOps.pad(im, (4, 1))
assert new_im.load()[2, 0] == 1
new_im = ImageOps.pad(im, (1, 4))
assert new_im.load()[0, 2] == 1
@pytest.mark.parametrize("mode", ("P", "PA"))
def test_palette(mode):
im = hopper(mode)
# Expand
expanded_im = ImageOps.expand(im)
assert_image_equal(im.convert("RGB"), expanded_im.convert("RGB"))
# Pad
padded_im = ImageOps.pad(im, (256, 128), centering=(0, 0))
assert_image_equal(
im.convert("RGB"), padded_im.convert("RGB").crop((0, 0, 128, 128))
)
def test_pil163():
# Division by zero in equalize if < 255 pixels in image (@PIL163)

View File

@ -50,6 +50,16 @@ def test_getcolor():
palette.getcolor("unknown")
def test_getcolor_rgba_color_rgb_palette():
palette = ImagePalette.ImagePalette("RGB")
# Opaque RGBA colors are converted
assert palette.getcolor((0, 0, 0, 255)) == palette.getcolor((0, 0, 0))
with pytest.raises(ValueError):
palette.getcolor((0, 0, 0, 128))
@pytest.mark.parametrize(
"index, palette",
[

View File

@ -45,10 +45,10 @@ def test_viewer_show(order):
not on_ci() or is_win32(),
reason="Only run on CIs; hangs on Windows CIs",
)
def test_show():
for mode in ("1", "I;16", "LA", "RGB", "RGBA"):
im = hopper(mode)
assert ImageShow.show(im)
@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA"))
def test_show(mode):
im = hopper(mode)
assert ImageShow.show(im)
def test_show_without_viewers():
@ -70,12 +70,12 @@ def test_viewer():
viewer.get_command(None)
def test_viewers():
for viewer in ImageShow._viewers:
try:
viewer.get_command("test.jpg")
except NotImplementedError:
pass
@pytest.mark.parametrize("viewer", ImageShow._viewers)
def test_viewers(viewer):
try:
viewer.get_command("test.jpg")
except NotImplementedError:
pass
def test_ipythonviewer():
@ -95,14 +95,14 @@ def test_ipythonviewer():
not on_ci() or is_win32(),
reason="Only run on CIs; hangs on Windows CIs",
)
def test_file_deprecated(tmp_path):
@pytest.mark.parametrize("viewer", ImageShow._viewers)
def test_file_deprecated(tmp_path, viewer):
f = str(tmp_path / "temp.jpg")
for viewer in ImageShow._viewers:
hopper().save(f)
with pytest.warns(DeprecationWarning):
try:
viewer.show_file(file=f)
except NotImplementedError:
pass
with pytest.raises(TypeError):
viewer.show_file()
hopper().save(f)
with pytest.warns(DeprecationWarning):
try:
viewer.show_file(file=f)
except NotImplementedError:
pass
with pytest.raises(TypeError):
viewer.show_file()

View File

@ -54,32 +54,39 @@ def test_kw():
assert im is None
def test_photoimage():
for mode in TK_MODES:
# test as image:
im = hopper(mode)
@pytest.mark.parametrize("mode", TK_MODES)
def test_photoimage(mode):
# test as image:
im = hopper(mode)
# this should not crash
# this should not crash
im_tk = ImageTk.PhotoImage(im)
assert im_tk.width() == im.width
assert im_tk.height() == im.height
reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded, im.convert("RGBA"))
def test_photoimage_apply_transparency():
with Image.open("Tests/images/pil123p.png") as im:
im_tk = ImageTk.PhotoImage(im)
assert im_tk.width() == im.width
assert im_tk.height() == im.height
reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded, im.convert("RGBA"))
def test_photoimage_blank():
@pytest.mark.parametrize("mode", TK_MODES)
def test_photoimage_blank(mode):
# test a image using mode/size:
for mode in TK_MODES:
im_tk = ImageTk.PhotoImage(mode, (100, 100))
im_tk = ImageTk.PhotoImage(mode, (100, 100))
assert im_tk.width() == 100
assert im_tk.height() == 100
assert im_tk.width() == 100
assert im_tk.height() == 100
im = Image.new(mode, (100, 100))
reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded.convert(mode), im)
im = Image.new(mode, (100, 100))
reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded.convert(mode), im)
def test_box_deprecation():

View File

@ -1,3 +1,5 @@
import pytest
from PIL import Image
from .helper import hopper
@ -20,65 +22,56 @@ def verify(im1):
), f"got {repr(p1)} from mode {im1.mode} at {xy}, expected {repr(p2)}"
def test_basic(tmp_path):
@pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I"))
def test_basic(tmp_path, mode):
# PIL 1.1 has limited support for 16-bit image data. Check that
# create/copy/transform and save works as expected.
def basic(mode):
im_in = original.convert(mode)
verify(im_in)
im_in = original.convert(mode)
verify(im_in)
w, h = im_in.size
w, h = im_in.size
im_out = im_in.copy()
verify(im_out) # copy
im_out = im_in.copy()
verify(im_out) # copy
im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
verify(im_out) # transform
im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
verify(im_out) # transform
filename = str(tmp_path / "temp.im")
im_in.save(filename)
filename = str(tmp_path / "temp.im")
im_in.save(filename)
with Image.open(filename) as im_out:
verify(im_in)
verify(im_out)
im_out = im_in.crop((0, 0, w, h))
verify(im_out)
im_out = Image.new(mode, (w, h), None)
im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0))
im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0))
with Image.open(filename) as im_out:
verify(im_in)
verify(im_out)
im_in = Image.new(mode, (1, 1), 1)
assert im_in.getpixel((0, 0)) == 1
im_out = im_in.crop((0, 0, w, h))
verify(im_out)
im_in.putpixel((0, 0), 2)
assert im_in.getpixel((0, 0)) == 2
im_out = Image.new(mode, (w, h), None)
im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0))
im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0))
if mode == "L":
maximum = 255
else:
maximum = 32767
verify(im_in)
verify(im_out)
im_in = Image.new(mode, (1, 1), 256)
assert im_in.getpixel((0, 0)) == min(256, maximum)
im_in = Image.new(mode, (1, 1), 1)
assert im_in.getpixel((0, 0)) == 1
im_in.putpixel((0, 0), 512)
assert im_in.getpixel((0, 0)) == min(512, maximum)
im_in.putpixel((0, 0), 2)
assert im_in.getpixel((0, 0)) == 2
basic("L")
if mode == "L":
maximum = 255
else:
maximum = 32767
basic("I;16")
basic("I;16B")
basic("I;16L")
im_in = Image.new(mode, (1, 1), 256)
assert im_in.getpixel((0, 0)) == min(256, maximum)
basic("I")
im_in.putpixel((0, 0), 512)
assert im_in.getpixel((0, 0)) == min(512, maximum)
def test_tobytes():

View File

@ -137,19 +137,9 @@ def test_save_tiff_uint16():
assert img_px[0, 0] == pixel_value
def test_to_array():
def _to_array(mode, dtype):
img = hopper(mode)
# Resize to non-square
img = img.crop((3, 0, 124, 127))
assert img.size == (121, 127)
np_img = numpy.array(img)
_test_img_equals_nparray(img, np_img)
assert np_img.dtype == dtype
modes = [
@pytest.mark.parametrize(
"mode, dtype",
(
("L", numpy.uint8),
("I", numpy.int32),
("F", numpy.float32),
@ -163,10 +153,18 @@ def test_to_array():
("I;16B", ">u2"),
("I;16L", "<u2"),
("HSV", numpy.uint8),
]
),
)
def test_to_array(mode, dtype):
img = hopper(mode)
for mode in modes:
_to_array(*mode)
# Resize to non-square
img = img.crop((3, 0, 124, 127))
assert img.size == (121, 127)
np_img = numpy.array(img)
_test_img_equals_nparray(img, np_img)
assert np_img.dtype == dtype
def test_point_lut():

View File

@ -60,11 +60,11 @@ def helper_pickle_string(pickle, protocol, test_file, mode):
("Tests/images/itxt_chunks.png", None),
],
)
def test_pickle_image(tmp_path, test_file, test_mode):
@pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1))
def test_pickle_image(tmp_path, test_file, test_mode, protocol):
# Act / Assert
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
helper_pickle_string(pickle, protocol, test_file, test_mode)
helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode)
helper_pickle_string(pickle, protocol, test_file, test_mode)
helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode)
def test_pickle_la_mode_with_palette(tmp_path):

View File

@ -43,8 +43,7 @@ clean:
-rm -rf $(BUILDDIR)/*
install-sphinx:
$(PYTHON) -c "import sphinx" > /dev/null 2>&1 || $(PYTHON) -m pip install sphinx
$(PYTHON) -c "import furo" > /dev/null 2>&1 || $(PYTHON) -m pip install furo
$(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo olefile
html:
$(MAKE) install-sphinx

View File

@ -178,6 +178,8 @@ Image.coerce_e
This undocumented method has been deprecated and will be removed in Pillow 10
(2023-07-01).
.. _Font size and offset methods:
Font size and offset methods
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -197,6 +199,40 @@ Deprecated Use
:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength`
=========================================================================== =============================================================================================================
Previous code:
.. code-block:: python
from PIL import Image, ImageDraw, ImageFont
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
width, height = font.getsize("Hello world")
left, top = font.getoffset("Hello world")
im = Image.new("RGB", (100, 100))
draw = ImageDraw.Draw(im)
width, height = draw.textsize("Hello world")
width, height = font.getsize_multiline("Hello\nworld")
width, height = draw.multiline_textsize("Hello\nworld")
Use instead:
.. code-block:: python
from PIL import Image, ImageDraw, ImageFont
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
left, top, right, bottom = font.getbbox("Hello world")
width, height = right - left, bottom - top
im = Image.new("RGB", (100, 100))
draw = ImageDraw.Draw(im)
width = draw.textlength("Hello world")
left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld")
width, height = right - left, bottom - top
Removed features
----------------
@ -253,7 +289,7 @@ Support for FreeType 2.7 has been removed.
We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe
vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`).
.. _FreeType: https://www.freetype.org
.. _FreeType: https://freetype.org/
im.offset
~~~~~~~~~

View File

@ -60,7 +60,10 @@ Pillow also provides limited support for a few additional modes, including:
* ``BGR;24`` (24-bit reversed true colour)
* ``BGR;32`` (32-bit reversed true colour)
However, Pillow doesnt support user-defined modes; if you need to handle band
Apart from these additional modes, Pillow doesn't yet support multichannel
images with a depth of more than 8 bits per channel.
Pillow also doesnt support user-defined modes; if you need to handle band
combinations that are not listed above, use a sequence of Image objects.
You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode`

View File

@ -31,6 +31,9 @@ BLP is the Blizzard Mipmap Format, a texture format used in World of
Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1``
images, and all types of ``BLP2`` images.
Saving
~~~~~~
Pillow supports writing BLP images. The :py:meth:`~PIL.Image.Image.save` method
can take the following keyword arguments:
@ -46,6 +49,9 @@ or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length enc
is not supported. Support for reading 8-bit run-length encoding was added in Pillow
9.1.0.
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@ -78,6 +84,9 @@ EPS images. The EPS driver can read EPS images in ``L``, ``LAB``, ``RGB`` and
than leaving them in the original color space. The EPS driver can write images
in ``L``, ``RGB`` and ``CMYK`` modes.
Loading
~~~~~~~
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
method with the following parameters to affect how Ghostscript renders the EPS
@ -134,6 +143,11 @@ To restore the default behavior, where ``P`` mode images are only converted to
from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
.. _gif-opening:
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@ -171,6 +185,8 @@ to seek to the next frame (``im.seek(im.tell() + 1)``).
``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame.
.. _gif-saving:
Saving
~~~~~~
@ -278,6 +294,11 @@ sets the following :py:attr:`~PIL.Image.Image.info` property:
ask for ``(512, 512, 2)``, the final value of
:py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``).
.. _icns-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
**append_images**
@ -292,6 +313,11 @@ ICO
ICO is used to store icons on Windows. The largest available icon is read.
.. _ico-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**sizes**
@ -337,6 +363,11 @@ their original size while loading them.
By default Pillow doesn't allow loading of truncated JPEG files, set
:data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this.
.. _jpeg-opening:
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method may set the following
:py:attr:`~PIL.Image.Image.info` properties if available:
@ -383,6 +414,10 @@ The :py:meth:`~PIL.Image.open` method may set the following
.. versionadded:: 7.1.0
.. _jpeg-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
@ -464,6 +499,11 @@ itself. It is also possible to set ``reduce`` to the number of resolutions to
discard (each one reduces the size of the resulting image by a factor of 2),
and ``layers`` to specify the number of quality layers to load.
.. _jpeg-2000-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**offset**
@ -575,6 +615,11 @@ called.
By default Pillow doesn't allow loading of truncated PNG files, set
:data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this.
.. _png-opening:
Opening
~~~~~~~
The :py:func:`~PIL.Image.open` function sets the following
:py:attr:`~PIL.Image.Image.info` properties, when appropriate:
@ -613,6 +658,11 @@ decompression bombs. Additionally, the total size of all of the text
chunks is limited to :data:`.PngImagePlugin.MAX_TEXT_MEMORY`, defaulting to
64MB.
.. _png-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**optimize**
@ -803,6 +853,11 @@ Pillow also reads SPIDER stack files containing sequences of SPIDER images. The
:py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods are supported, and
random access is allowed.
.. _spider-opening:
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method sets the following attributes:
**format**
@ -819,8 +874,10 @@ is provided for converting floating point data to byte data (mode ``L``)::
im = Image.open("image001.spi").convert2byte()
Writing files in SPIDER format
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. _spider-saving:
Saving
~~~~~~
The extension of SPIDER files may be any 3 alphanumeric characters. Therefore
the output format must be specified explicitly::
@ -837,6 +894,11 @@ Pillow reads and writes TGA images containing ``L``, ``LA``, ``P``,
``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and
run-length encoded TGAs.
.. _tga-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
**compression**
@ -871,6 +933,11 @@ uncompressed files.
support for reading Packbits, LZW and JPEG compressed TIFFs
without using libtiff.
.. _tiff-opening:
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@ -922,8 +989,10 @@ and can be accessed in any order.
``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the
last frame.
Saving Tiff Images
~~~~~~~~~~~~~~~~~~
.. _tiff-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
@ -1035,6 +1104,11 @@ WebP
Pillow reads and writes WebP files. The specifics of Pillow's capabilities with
this format are currently undocumented.
.. _webp-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**lossless**
@ -1058,7 +1132,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
the system WebP library was built with webpmux support.
Saving sequences
~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~
.. note::
@ -1173,6 +1247,11 @@ GBR
The GBR decoder reads GIMP brush files, version 1 and 2.
.. _gbr-opening:
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@ -1188,6 +1267,11 @@ GD
Pillow reads uncompressed GD2 files. Note that you must use
:py:func:`PIL.GdImageFile.open` to read such a file.
.. _gd-opening:
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@ -1227,6 +1311,11 @@ image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL
methods may be used to read other pictures from the file. The pictures are
zero-indexed and random access is supported.
.. _mpo-saving:
Saving
~~~~~~
When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default
only the first frame of a multiframe image will be saved. If the ``save_all``
argument is present and true, then all frames will be saved, and the following
@ -1326,6 +1415,11 @@ XPM
Pillow reads X pixmap files (mode ``P``) with 256 colors or less.
.. _xpm-opening:
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@ -1350,6 +1444,11 @@ Pillow can write PDF (Acrobat) images. Such images are written as binary PDF 1.4
files, using either JPEG or HEX encoding depending on the image mode (and
whether JPEG support is available or not).
.. _pdf-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
**save_all**

View File

@ -69,6 +69,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://pypi.org/project/Pillow/
:alt: Number of PyPI downloads
.. image:: https://bestpractices.coreinfrastructure.org/projects/6331/badge
:target: https://bestpractices.coreinfrastructure.org/projects/6331
:alt: OpenSSF Best Practices
Overview
========

View File

@ -184,7 +184,7 @@ Many of Pillow's features require external libraries:
loads libfribidi at runtime if it is installed.
On Windows this requires compiling FriBiDi and installing ``fribidi.dll``
into a directory listed in the `Dynamic-Link Library Search Order (Microsoft Docs)
<https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#search-order-for-desktop-applications>`_
<https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#search-order-for-desktop-applications>`_
(``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected).
See `Build Options`_ to see how to build this version.
* Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime.

View File

@ -10,7 +10,7 @@ provide constants and clear-text names for various well-known EXIF tags.
.. py:data:: TAGS
:type: dict
The TAG dictionary maps 16-bit integer EXIF tag enumerations to
The TAGS dictionary maps 16-bit integer EXIF tag enumerations to
descriptive string names. For instance:
>>> from PIL.ExifTags import TAGS

View File

@ -285,8 +285,8 @@ Methods
Draws a rectangle.
:param xy: Two points to define the bounding box. Sequence of either
``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The second point
is just outside the drawn rectangle.
``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box
is inclusive of both endpoints.
:param outline: Color to use for the outline.
:param fill: Color to use for the fill.
:param width: The line width, in pixels.
@ -298,8 +298,8 @@ Methods
Draws a rounded rectangle.
:param xy: Two points to define the bounding box. Sequence of either
``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The second point
is just outside the drawn rectangle.
``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box
is inclusive of both endpoints.
:param radius: Radius of the corners.
:param outline: Color to use for the outline.
:param fill: Color to use for the fill.
@ -443,6 +443,8 @@ Methods
.. deprecated:: 9.2.0
See :ref:`deprecations <Font size and offset methods>` for more information.
Use :py:meth:`textlength()` to measure the offset of following text with
1/64 pixel precision.
Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor.
@ -489,10 +491,14 @@ Methods
.. versionadded:: 6.2.0
:return: (width, height)
.. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0)
.. deprecated:: 9.2.0
See :ref:`deprecations <Font size and offset methods>` for more information.
Use :py:meth:`.multiline_textbbox` instead.
Return the size of the given string, in pixels.
@ -541,6 +547,8 @@ Methods
.. versionadded:: 6.2.0
:return: (width, height)
.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False)
Returns length (in pixels with 1/64 precision) of given text when rendered
@ -608,6 +616,7 @@ Methods
It should be a `BCP 47 language code`_.
Requires libraqm.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
:return: Width for horizontal, height for vertical text.
.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
@ -657,6 +666,7 @@ Methods
Requires libraqm.
:param stroke_width: The width of the text stroke.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
:return: ``(left, top, right, bottom)`` bounding box
.. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
@ -700,6 +710,7 @@ Methods
Requires libraqm.
:param stroke_width: The width of the text stroke.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
:return: ``(left, top, right, bottom)`` bounding box
.. py:method:: getdraw(im=None, hints=None)
@ -731,4 +742,4 @@ Methods
homogeneous, but similar, colors.
.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/
.. _OpenType docs: https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist

View File

@ -105,7 +105,7 @@ Resolve confusion getting PIL / Pillow version string
Re: "version constants deprecated" listed above, as user gnbl notes in #3082:
- it's confusing that PIL.VERSION returns the version string of the former PIL instead of Pillow's
- there does not seem to be documentation on this version number (why this, will it ever change, ..) e.g. at https://pillow.readthedocs.io/en/5.1.x/about.html#why-a-fork
- ReadTheDocs documentation is missing for some version branches (why is this, will it ever change, ...)
- it's confusing that PIL.version is a module and does not return the version information directly or hints on how to get it
- the package information header is essentially useless (placeholder, does not even mention Pillow, nor the version)
- PIL._version module documentation comment could explain how to access the version information

View File

@ -59,6 +59,40 @@ Deprecated Use
:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength`
=========================================================================== =============================================================================================================
Previous code:
.. code-block:: python
from PIL import Image, ImageDraw, ImageFont
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
width, height = font.getsize("Hello world")
left, top = font.getoffset("Hello world")
im = Image.new("RGB", (100, 100))
draw = ImageDraw.Draw(im)
width, height = draw.textsize("Hello world")
width, height = font.getsize_multiline("Hello\nworld")
width, height = draw.multiline_textsize("Hello\nworld")
Use instead:
.. code-block:: python
from PIL import Image, ImageDraw, ImageFont
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
left, top, right, bottom = font.getbbox("Hello world")
width, height = right - left, bottom - top
im = Image.new("RGB", (100, 100))
draw = ImageDraw.Draw(im)
width = draw.textlength("Hello world")
left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld")
width, height = right - left, bottom - top
API Additions
=============

View File

@ -11,7 +11,7 @@ Pillow follows `Semantic Versioning <https://semver.org/>`_:
2. MINOR version when you add functionality in a backwards compatible manner, and
3. PATCH version when you make backwards compatible bug fixes.
Quarterly releases ("`Main Release <https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#main-release>`_")
Quarterly releases ("`Main Release <https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#user-content-main-release>`_")
bump at least the MINOR version, as new functionality has likely been added in the
prior three months.
@ -21,8 +21,8 @@ these occur every 12-18 months, guided by
`Python's EOL schedule <https://devguide.python.org/#status-of-python-branches>`_, and
any APIs that have been deprecated for at least a year are removed at the same time.
PATCH versions ("`Point Release <https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#point-release>`_"
or "`Embargoed Release <https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#embargoed-release>`_")
PATCH versions ("`Point Release <https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#user-content-point-release>`_"
or "`Embargoed Release <https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#user-content-embargoed-release>`_")
are for security, installation or critical bug fixes. These are less common as it is
preferred to stick to quarterly releases.

View File

@ -375,6 +375,16 @@ def _save(im, fp, filename, bitmap_header=True):
header = 40 # or 64 for OS/2 version 2
image = stride * im.size[1]
if im.mode == "1":
palette = b"".join(o8(i) * 4 for i in (0, 255))
elif im.mode == "L":
palette = b"".join(o8(i) * 4 for i in range(256))
elif im.mode == "P":
palette = im.im.getpalette("RGB", "BGRX")
colors = len(palette) // 4
else:
palette = None
# bitmap header
if bitmap_header:
offset = 14 + header + colors * 4
@ -405,14 +415,8 @@ def _save(im, fp, filename, bitmap_header=True):
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
if im.mode == "1":
for i in (0, 255):
fp.write(o8(i) * 4)
elif im.mode == "L":
for i in range(256):
fp.write(o8(i) * 4)
elif im.mode == "P":
fp.write(im.im.getpalette("RGB", "BGRX"))
if palette:
fp.write(palette)
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))])

View File

@ -101,6 +101,8 @@ DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29
DXGI_FORMAT_BC5_TYPELESS = 82
DXGI_FORMAT_BC5_UNORM = 83
DXGI_FORMAT_BC5_SNORM = 84
DXGI_FORMAT_BC6H_UF16 = 95
DXGI_FORMAT_BC6H_SF16 = 96
DXGI_FORMAT_BC7_TYPELESS = 97
DXGI_FORMAT_BC7_UNORM = 98
DXGI_FORMAT_BC7_UNORM_SRGB = 99
@ -181,6 +183,14 @@ class DdsImageFile(ImageFile.ImageFile):
self.pixel_format = "BC5S"
n = 5
self.mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_UF16:
self.pixel_format = "BC6H"
n = 6
self.mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_SF16:
self.pixel_format = "BC6HS"
n = 6
self.mode = "RGB"
elif dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM):
self.pixel_format = "BC7"
n = 7

View File

@ -15,6 +15,7 @@
# See the README file for information on usage and redistribution.
#
import os
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
@ -80,11 +81,19 @@ class FliImageFile(ImageFile.ImageFile):
if i16(s, 4) == 0xF1FA:
# look for palette chunk
s = self.fp.read(6)
if i16(s, 4) == 11:
self._palette(palette, 2)
elif i16(s, 4) == 4:
self._palette(palette, 0)
number_of_subchunks = i16(s, 6)
chunk_size = None
for _ in range(number_of_subchunks):
if chunk_size is not None:
self.fp.seek(chunk_size - 6, os.SEEK_CUR)
s = self.fp.read(6)
chunk_type = i16(s, 4)
if chunk_type in (4, 11):
self._palette(palette, 2 if chunk_type == 11 else 0)
break
chunk_size = i32(s)
if not chunk_size:
break
palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette]
self.palette = ImagePalette.raw("RGB", b"".join(palette))

View File

@ -301,11 +301,13 @@ class GifImageFile(ImageFile.ImageFile):
self.im.paste(self.dispose, self.dispose_extent)
self._frame_palette = palette if palette is not None else self.global_palette
self._frame_transparency = frame_transparency
if frame == 0:
if self._frame_palette:
self.mode = (
"RGB" if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS else "P"
)
if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
self.mode = "RGBA" if frame_transparency is not None else "RGB"
else:
self.mode = "P"
else:
self.mode = "L"
@ -315,7 +317,6 @@ class GifImageFile(ImageFile.ImageFile):
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
@ -388,7 +389,8 @@ class GifImageFile(ImageFile.ImageFile):
transparency = -1
if frame_transparency is not None:
if frame == 0:
self.info["transparency"] = frame_transparency
if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS:
self.info["transparency"] = frame_transparency
elif self.mode not in ("RGB", "RGBA"):
transparency = frame_transparency
self.tile = [
@ -412,9 +414,9 @@ class GifImageFile(ImageFile.ImageFile):
temp_mode = "P" if self._frame_palette else "L"
self._prev_im = None
if self.__frame == 0:
if "transparency" in self.info:
if self._frame_transparency is not None:
self.im = Image.core.fill(
temp_mode, self.size, self.info["transparency"]
temp_mode, self.size, self._frame_transparency
)
elif self.mode in ("RGB", "RGBA"):
self._prev_im = self.im
@ -431,8 +433,12 @@ class GifImageFile(ImageFile.ImageFile):
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)
if self._frame_transparency is not None:
self.im.putpalettealpha(self._frame_transparency, 0)
self.mode = "RGBA"
else:
self.mode = "RGB"
self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
return
if not self._prev_im:
return
@ -518,9 +524,8 @@ def _normalize_palette(im, palette, info):
used_palette_colors = []
for i in range(0, len(source_palette), 3):
source_color = tuple(source_palette[i : i + 3])
try:
index = im.palette.colors[source_color]
except KeyError:
index = im.palette.colors.get(source_color)
if index in used_palette_colors:
index = None
used_palette_colors.append(index)
for i, index in enumerate(used_palette_colors):

View File

@ -352,7 +352,13 @@ def _save(im, fp, filename):
fp.write(b"Lut: 1\r\n")
fp.write(b"\000" * (511 - fp.tell()) + b"\032")
if im.mode in ["P", "PA"]:
fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes
im_palette = im.im.getpalette("RGB", "RGB;L")
colors = len(im_palette) // 3
palette = b""
for i in range(3):
palette += im_palette[colors * i : colors * (i + 1)]
palette += b"\x00" * (256 - colors)
fp.write(palette) # 768 bytes
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))])

View File

@ -679,12 +679,24 @@ class Image:
new["shape"] = shape
new["typestr"] = typestr
new["version"] = 3
if self.mode == "1":
# Binary images need to be extended from bits to bytes
# See: https://github.com/python-pillow/Pillow/issues/350
new["data"] = self.tobytes("raw", "L")
else:
new["data"] = self.tobytes()
try:
if self.mode == "1":
# Binary images need to be extended from bits to bytes
# See: https://github.com/python-pillow/Pillow/issues/350
new["data"] = self.tobytes("raw", "L")
else:
new["data"] = self.tobytes()
except Exception as e:
if not isinstance(e, (MemoryError, RecursionError)):
try:
import numpy
from packaging.version import parse as parse_version
except ImportError:
pass
else:
if parse_version(numpy.__version__) < parse_version("1.23"):
warnings.warn(e)
raise
return new
def __getstate__(self):
@ -1949,11 +1961,7 @@ class Image:
m_im = m_im.convert("L")
# Internally, we require 256 palette entries.
new_palette_bytes = (
palette_bytes + ((256 * bands) - len(palette_bytes)) * b"\x00"
)
m_im.putpalette(new_palette_bytes, palette_mode)
m_im.putpalette(palette_bytes, palette_mode)
m_im.palette = ImagePalette.ImagePalette(palette_mode, palette=palette_bytes)
if "transparency" in self.info:

View File

@ -482,8 +482,8 @@ class ImageDraw:
# extract mask and set text alpha
color, mask = mask, mask.getband(3)
color.fillband(3, (ink >> 24) & 0xFF)
coord2 = coord[0] + mask.size[0], coord[1] + mask.size[1]
self.im.paste(color, coord + coord2, mask)
x, y = (int(c) for c in coord)
self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask)
else:
self.draw.draw_bitmap(coord, mask, ink)

View File

@ -141,6 +141,8 @@ class ImageFont:
Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead.
See :ref:`deprecations <Font size and offset methods>` for more information.
Returns width and height (in pixels) of given text.
:param text: Text to measure.
@ -338,7 +340,7 @@ class FreeTypeFont:
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
:param language: Language of the text. Different languages may use
@ -391,7 +393,7 @@ class FreeTypeFont:
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
:param language: Language of the text. Different languages may use
@ -432,6 +434,8 @@ class FreeTypeFont:
1/64 pixel precision.
Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor.
See :ref:`deprecations <Font size and offset methods>` for more information.
Returns width and height (in pixels) of given text if rendered in font with
provided direction, features, and language.
@ -456,7 +460,7 @@ class FreeTypeFont:
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
.. versionadded:: 4.2.0
@ -500,6 +504,8 @@ class FreeTypeFont:
Use :py:meth:`.ImageDraw.multiline_textbbox` instead.
See :ref:`deprecations <Font size and offset methods>` for more information.
Returns width and height (in pixels) of given text if rendered in font
with provided direction, features, and language, while respecting
newline characters.
@ -520,7 +526,7 @@ class FreeTypeFont:
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
:param language: Language of the text. Different languages may use
@ -559,6 +565,8 @@ class FreeTypeFont:
Use :py:meth:`.getbbox` instead.
See :ref:`deprecations <Font size and offset methods>` for more information.
Returns the offset of given text. This is the gap between the
starting coordinate and the first marking. Note that this gap is
included in the result of :py:func:`~PIL.ImageFont.FreeTypeFont.getsize`.
@ -610,7 +618,7 @@ class FreeTypeFont:
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
.. versionadded:: 4.2.0
@ -702,7 +710,7 @@ class FreeTypeFont:
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
.. versionadded:: 4.2.0
@ -852,6 +860,8 @@ class TransposedFont:
.. deprecated:: 9.2.0
Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead.
See :ref:`deprecations <Font size and offset methods>` for more information.
"""
deprecate("getsize", 10, "getbbox or getlength")
with warnings.catch_warnings():
@ -945,6 +955,11 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
encoding of any text provided in subsequent operations.
:param layout_engine: Which layout engine to use, if available:
:data:`.ImageFont.Layout.BASIC` or :data:`.ImageFont.Layout.RAQM`.
If it is available, Raqm layout will be used by default.
Otherwise, basic layout will be used.
Raqm layout is recommended for all non-English text. If Raqm layout
is not required, basic layout will have better performance.
You can check support for Raqm layout using
:py:func:`PIL.features.check_feature` with ``feature="raqm"``.

View File

@ -21,7 +21,7 @@ import functools
import operator
import re
from . import Image
from . import Image, ImagePalette
#
# helpers
@ -255,11 +255,11 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
if im_ratio != dest_ratio:
if im_ratio > dest_ratio:
new_height = int(image.height / image.width * size[0])
new_height = round(image.height / image.width * size[0])
if new_height != size[1]:
size = (size[0], new_height)
else:
new_width = int(image.width / image.height * size[1])
new_width = round(image.width / image.height * size[1])
if new_width != size[0]:
size = (new_width, size[1])
return image.resize(size, resample=method)
@ -291,11 +291,13 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5
out = resized
else:
out = Image.new(image.mode, size, color)
if resized.palette:
out.putpalette(resized.getpalette())
if resized.width != size[0]:
x = int((size[0] - resized.width) * max(0, min(centering[0], 1)))
x = round((size[0] - resized.width) * max(0, min(centering[0], 1)))
out.paste(resized, (x, 0))
else:
y = int((size[1] - resized.height) * max(0, min(centering[1], 1)))
y = round((size[1] - resized.height) * max(0, min(centering[1], 1)))
out.paste(resized, (0, y))
return out
@ -396,9 +398,8 @@ def expand(image, border=0, fill=0):
width = left + image.size[0] + right
height = top + image.size[1] + bottom
color = _color(fill, image.mode)
if image.mode == "P" and image.palette:
image.load()
palette = image.palette.copy()
if image.palette:
palette = ImagePalette.ImagePalette(palette=image.getpalette())
if isinstance(color, tuple):
color = palette.getcolor(color)
else:

View File

@ -50,15 +50,24 @@ class ImagePalette:
@palette.setter
def palette(self, palette):
self._colors = None
self._palette = palette
mode_len = len(self.mode)
self.colors = {}
for i in range(0, len(self.palette), mode_len):
color = tuple(self.palette[i : i + mode_len])
if color in self.colors:
continue
self.colors[color] = i // mode_len
@property
def colors(self):
if self._colors is None:
mode_len = len(self.mode)
self._colors = {}
for i in range(0, len(self.palette), mode_len):
color = tuple(self.palette[i : i + mode_len])
if color in self._colors:
continue
self._colors[color] = i // mode_len
return self._colors
@colors.setter
def colors(self, colors):
self._colors = colors
def copy(self):
new = ImagePalette()
@ -106,7 +115,11 @@ class ImagePalette:
raise ValueError("palette contains raw palette data")
if isinstance(color, tuple):
if self.mode == "RGB":
if len(color) == 4 and color[3] == 255:
if len(color) == 4:
if color[3] != 255:
raise ValueError(
"cannot add non-opaque RGBA color to RGB palette"
)
color = color[:3]
elif self.mode == "RGBA":
if len(color) == 3:

View File

@ -136,7 +136,7 @@ class WindowsViewer(Viewer):
"""The default viewer on Windows is the default system application for PNG files."""
format = "PNG"
options = {"compress_level": 1}
options = {"compress_level": 1, "save_all": True}
def get_command(self, file, **options):
return (
@ -154,7 +154,7 @@ class MacViewer(Viewer):
"""The default viewer on macOS using ``Preview.app``."""
format = "PNG"
options = {"compress_level": 1}
options = {"compress_level": 1, "save_all": True}
def get_command(self, file, **options):
# on darwin open returns immediately resulting in the temp
@ -197,7 +197,7 @@ if sys.platform == "darwin":
class UnixViewer(Viewer):
format = "PNG"
options = {"compress_level": 1}
options = {"compress_level": 1, "save_all": True}
def get_command(self, file, **options):
command = self.get_command_ex(file, **options)[0]

View File

@ -107,6 +107,7 @@ class PhotoImage:
mode = image.mode
if mode == "P":
# palette mapped data
image.apply_transparency()
image.load()
try:
mode = image.palette.mode

View File

@ -39,15 +39,19 @@ class ImtImageFile(ImageFile.ImageFile):
# Quick rejection: if there's not a LF among the first
# 100 bytes, this is (probably) not a text header.
if b"\n" not in self.fp.read(100):
buffer = self.fp.read(100)
if b"\n" not in buffer:
raise SyntaxError("not an IM file")
self.fp.seek(0)
xsize = ysize = 0
while True:
s = self.fp.read(1)
if buffer:
s = buffer[:1]
buffer = buffer[1:]
else:
s = self.fp.read(1)
if not s:
break
@ -55,7 +59,12 @@ class ImtImageFile(ImageFile.ImageFile):
# image data begins
self.tile = [
("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1))
(
"raw",
(0, 0) + self.size,
self.fp.tell() - len(buffer),
(self.mode, 0, 1),
)
]
break
@ -63,8 +72,11 @@ class ImtImageFile(ImageFile.ImageFile):
else:
# read key/value pair
# FIXME: dangerous, may read whole file
s = s + self.fp.readline()
if b"\n" not in buffer:
buffer += self.fp.read(100)
lines = buffer.split(b"\n")
s += lines.pop(0)
buffer = b"\n".join(lines)
if len(s) == 1 or len(s) > 100:
break
if s[0] == ord(b"*"):
@ -74,13 +86,13 @@ class ImtImageFile(ImageFile.ImageFile):
if not m:
break
k, v = m.group(1, 2)
if k == "width":
if k == b"width":
xsize = int(v)
self._size = xsize, ysize
elif k == "height":
elif k == b"height":
ysize = int(v)
self._size = xsize, ysize
elif k == "pixel" and v == "n8":
elif k == b"pixel" and v == b"n8":
self.mode = "L"

View File

@ -193,9 +193,10 @@ def _save(im, fp, filename):
warnings.warn("id_section has been trimmed to 255 characters")
if colormaptype:
colormapfirst, colormaplength, colormapentry = 0, 256, 24
palette = im.im.getpalette("RGB", "BGR")
colormaplength, colormapentry = len(palette) // 3, 24
else:
colormapfirst, colormaplength, colormapentry = 0, 0, 0
colormaplength, colormapentry = 0, 0
if im.mode in ("LA", "RGBA"):
flags = 8
@ -210,7 +211,7 @@ def _save(im, fp, filename):
o8(id_len)
+ o8(colormaptype)
+ o8(imagetype)
+ o16(colormapfirst)
+ o16(0) # colormapfirst
+ o16(colormaplength)
+ o8(colormapentry)
+ o16(0)
@ -225,7 +226,7 @@ def _save(im, fp, filename):
fp.write(id_section)
if colormaptype:
fp.write(im.im.getpalette("RGB", "BGR"))
fp.write(palette)
if rle:
ImageFile._save(

View File

@ -173,6 +173,7 @@ OPEN_INFO = {
(II, 1, (1,), 2, (8,), ()): ("L", "L;R"),
(MM, 1, (1,), 2, (8,), ()): ("L", "L;R"),
(II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"),
(II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"),
(II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"),
(MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"),
(II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"),
@ -1148,6 +1149,39 @@ class TiffImageFile(ImageFile.ImageFile):
"""Return the current frame number"""
return self.__frame
def get_child_images(self):
if SUBIFD not in self.tag_v2:
return []
child_images = []
exif = self.getexif()
offset = None
for im_offset in self.tag_v2[SUBIFD]:
# reset buffered io handle in case fp
# was passed to libtiff, invalidating the buffer
current_offset = self._fp.tell()
if offset is None:
offset = current_offset
fp = self._fp
ifd = exif._get_ifd_dict(im_offset)
jpegInterchangeFormat = ifd.get(513)
if jpegInterchangeFormat is not None:
fp.seek(jpegInterchangeFormat)
jpeg_data = fp.read(ifd.get(514))
fp = io.BytesIO(jpeg_data)
with Image.open(fp) as im:
if jpegInterchangeFormat is None:
im._frame_pos = [im_offset]
im._seek(0)
im.load()
child_images.append(im)
if offset is not None:
self._fp.seek(offset)
return child_images
def getxmp(self):
"""
Returns a dictionary containing the XMP tags.

View File

@ -160,6 +160,7 @@ TAGS_V2 = {
323: ("TileLength", LONG, 1),
324: ("TileOffsets", LONG, 0),
325: ("TileByteCounts", LONG, 0),
330: ("SubIFDs", LONG, 0),
332: ("InkSet", SHORT, 1),
333: ("InkNames", ASCII, 1),
334: ("NumberOfInks", SHORT, 1),

View File

@ -311,9 +311,11 @@ def _save(im, fp, filename):
lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80)
icc_profile = im.encoderinfo.get("icc_profile") or ""
exif = im.encoderinfo.get("exif", "")
exif = im.encoderinfo.get("exif", b"")
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
if exif.startswith(b"Exif\x00\x00"):
exif = exif[6:]
xmp = im.encoderinfo.get("xmp", "")
method = im.encoderinfo.get("method", 4)

View File

@ -376,11 +376,8 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) {
actual = "L";
break;
case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */
actual = "RGB";
break;
case 6: /* BC6: 3-channel 16-bit float */
/* TODO: support 4-channel floating point images */
actual = "RGBAF";
actual = "RGB";
break;
default:
PyErr_SetString(PyExc_ValueError, "block compression type unknown");

View File

@ -23,10 +23,6 @@ typedef struct {
UINT8 l;
} lum;
typedef struct {
FLOAT32 r, g, b;
} rgb32f;
typedef struct {
UINT16 c0, c1;
UINT32 lut;
@ -536,53 +532,53 @@ static const bc6_mode_info bc6_modes[] = {
/* Table.F, encoded as a sequence of bit indices */
static const UINT8 bc6_bit_packings[][75] = {
{116, 132, 176, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17,
{116, 132, 180, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65,
66, 67, 68, 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 173, 128,
129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175},
{117, 164, 165, 0, 1, 2, 3, 4, 5, 6, 172, 173, 132, 16, 17,
18, 19, 20, 21, 22, 133, 174, 116, 32, 33, 34, 35, 36, 37, 38,
175, 177, 176, 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65,
66, 67, 68, 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128,
129, 130, 131, 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179},
{117, 164, 165, 0, 1, 2, 3, 4, 5, 6, 176, 177, 132, 16, 17,
18, 19, 20, 21, 22, 133, 178, 116, 32, 33, 34, 35, 36, 37, 38,
179, 181, 180, 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65,
66, 67, 68, 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128,
129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
48, 49, 50, 51, 52, 10, 112, 113, 114, 115, 64, 65, 66, 67, 26,
172, 160, 161, 162, 163, 80, 81, 82, 83, 42, 173, 128, 129, 130, 131,
96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175},
176, 160, 161, 162, 163, 80, 81, 82, 83, 42, 177, 128, 129, 130, 131,
96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
48, 49, 50, 51, 10, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68,
26, 160, 161, 162, 163, 80, 81, 82, 83, 42, 173, 128, 129, 130, 131,
96, 97, 98, 99, 172, 174, 144, 145, 146, 147, 116, 175},
26, 160, 161, 162, 163, 80, 81, 82, 83, 42, 177, 128, 129, 130, 131,
96, 97, 98, 99, 176, 178, 144, 145, 146, 147, 116, 179},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
48, 49, 50, 51, 10, 132, 112, 113, 114, 115, 64, 65, 66, 67, 26,
172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 42, 128, 129, 130, 131,
96, 97, 98, 99, 173, 174, 144, 145, 146, 147, 176, 175},
176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 42, 128, 129, 130, 131,
96, 97, 98, 99, 177, 178, 144, 145, 146, 147, 180, 179},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 132, 16, 17, 18, 19, 20,
21, 22, 23, 24, 116, 32, 33, 34, 35, 36, 37, 38, 39, 40, 176,
21, 22, 23, 24, 116, 32, 33, 34, 35, 36, 37, 38, 39, 40, 180,
48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68,
172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 173, 128, 129, 130, 131,
96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175},
176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131,
96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179},
{0, 1, 2, 3, 4, 5, 6, 7, 164, 132, 16, 17, 18, 19, 20,
21, 22, 23, 174, 116, 32, 33, 34, 35, 36, 37, 38, 39, 175, 176,
21, 22, 23, 178, 116, 32, 33, 34, 35, 36, 37, 38, 39, 179, 180,
48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68,
172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 173, 128, 129, 130, 131,
176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131,
96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149},
{0, 1, 2, 3, 4, 5, 6, 7, 172, 132, 16, 17, 18, 19, 20,
21, 22, 23, 117, 116, 32, 33, 34, 35, 36, 37, 38, 39, 165, 176,
{0, 1, 2, 3, 4, 5, 6, 7, 176, 132, 16, 17, 18, 19, 20,
21, 22, 23, 117, 116, 32, 33, 34, 35, 36, 37, 38, 39, 165, 180,
48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68,
69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 173, 128, 129, 130, 131,
96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175},
{0, 1, 2, 3, 4, 5, 6, 7, 173, 132, 16, 17, 18, 19, 20,
21, 22, 23, 133, 116, 32, 33, 34, 35, 36, 37, 38, 39, 177, 176,
69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131,
96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179},
{0, 1, 2, 3, 4, 5, 6, 7, 177, 132, 16, 17, 18, 19, 20,
21, 22, 23, 133, 116, 32, 33, 34, 35, 36, 37, 38, 39, 181, 180,
48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68,
172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131,
96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175},
{0, 1, 2, 3, 4, 5, 164, 172, 173, 132, 16, 17, 18, 19, 20,
21, 117, 133, 174, 116, 32, 33, 34, 35, 36, 37, 165, 175, 177, 176,
176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131,
96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179},
{0, 1, 2, 3, 4, 5, 164, 176, 177, 132, 16, 17, 18, 19, 20,
21, 117, 133, 178, 116, 32, 33, 34, 35, 36, 37, 165, 179, 181, 180,
48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68,
69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131,
96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149},
@ -681,20 +677,31 @@ bc6_finalize(int v, int sign) {
}
}
static UINT8
bc6_clamp(float value) {
if (value < 0.0f) {
return 0;
} else if (value > 1.0f) {
return 255;
} else {
return (UINT8) (value * 255.0f);
}
}
static void
bc6_lerp(rgb32f *col, int *e0, int *e1, int s, int sign) {
bc6_lerp(rgba *col, int *e0, int *e1, int s, int sign) {
int r, g, b;
int t = 64 - s;
r = (e0[0] * t + e1[0] * s) >> 6;
g = (e0[1] * t + e1[1] * s) >> 6;
b = (e0[2] * t + e1[2] * s) >> 6;
col->r = bc6_finalize(r, sign);
col->g = bc6_finalize(g, sign);
col->b = bc6_finalize(b, sign);
col->r = bc6_clamp(bc6_finalize(r, sign));
col->g = bc6_clamp(bc6_finalize(g, sign));
col->b = bc6_clamp(bc6_finalize(b, sign));
}
static void
decode_bc6_block(rgb32f *col, const UINT8 *src, int sign) {
decode_bc6_block(rgba *col, const UINT8 *src, int sign) {
UINT16 endpoints[12]; /* storage for r0, g0, b0, r1, ... */
int ueps[12];
int i, i0, ib2, di, dw, mask, numep, s;
@ -744,21 +751,16 @@ decode_bc6_block(rgb32f *col, const UINT8 *src, int sign) {
}
if (sign || info->tr) { /* sign-extend e1,2,3 if signed or deltas */
for (i = 3; i < numep; i += 3) {
bc6_sign_extend(&endpoints[i + 0], info->rb);
bc6_sign_extend(&endpoints[i], info->rb);
bc6_sign_extend(&endpoints[i + 1], info->gb);
bc6_sign_extend(&endpoints[i + 2], info->bb);
}
}
if (info->tr) { /* apply deltas */
for (i = 3; i < numep; i++) {
for (i = 3; i < numep; i += 3) {
endpoints[i] = (endpoints[i] + endpoints[0]) & mask;
}
if (sign) {
for (i = 3; i < numep; i += 3) {
bc6_sign_extend(&endpoints[i + 0], info->rb);
bc6_sign_extend(&endpoints[i + 1], info->gb);
bc6_sign_extend(&endpoints[i + 2], info->bb);
}
endpoints[i + 1] = (endpoints[i + 1] + endpoints[1]) & mask;
endpoints[i + 2] = (endpoints[i + 2] + endpoints[2]) & mask;
}
}
for (i = 0; i < numep; i++) {
@ -862,8 +864,8 @@ decode_bcn(
break;
case 6:
while (bytes >= 16) {
rgb32f col[16];
decode_bc6_block(col, ptr, (state->state >> 4) & 1);
rgba col[16];
decode_bc6_block(col, ptr, strcmp(pixel_format, "BC6HS") == 0 ? 1 : 0);
put_block(im, state, (const char *)col, sizeof(col[0]), C);
ptr += 16;
bytes -= 16;

View File

@ -432,18 +432,18 @@ fill_mask_L(
}
} else {
int alpha_channel = strcmp(imOut->mode, "RGBa") == 0 ||
strcmp(imOut->mode, "RGBA") == 0 ||
strcmp(imOut->mode, "La") == 0 ||
strcmp(imOut->mode, "LA") == 0 ||
strcmp(imOut->mode, "PA") == 0;
for (y = 0; y < ysize; y++) {
UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx * pixelsize;
UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx;
for (x = 0; x < xsize; x++) {
for (i = 0; i < pixelsize; i++) {
UINT8 channel_mask = *mask;
if ((strcmp(imOut->mode, "RGBa") == 0 ||
strcmp(imOut->mode, "RGBA") == 0 ||
strcmp(imOut->mode, "La") == 0 ||
strcmp(imOut->mode, "LA") == 0 ||
strcmp(imOut->mode, "PA") == 0) &&
i != 3 && channel_mask != 0) {
if (alpha_channel && i != 3 && channel_mask != 0) {
channel_mask =
255 - (255 - channel_mask) * (1 - (255 - out[3]) / 255);
}

View File

@ -24,7 +24,7 @@
*
* This cast is safe, as the top 32-bits of HFILE are guaranteed to be zero,
* see
* https://docs.microsoft.com/en-us/windows/win32/winprog64/interprocess-communication
* https://learn.microsoft.com/en-us/windows/win32/winprog64/interprocess-communication
*/
#ifndef USE_WIN32_FILEIO
#define fd_to_tiff_fd(fd) (fd)

View File

@ -81,5 +81,5 @@ The following projects have patches to support complex text layout using Raqm:
[1]: https://github.com/fribidi/fribidi
[2]: https://github.com/Tehreer/SheenBidi
[3]: https://github.com/harfbuzz/harfbuzz
[4]: https://www.freetype.org
[4]: https://freetype.org/
[5]: https://www.gtk.org/gtk-doc

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