mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-24 17:06:16 +03:00
Merge branch 'main' into gif
This commit is contained in:
commit
5f8938cb8e
|
@ -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
17
.github/renovate.json
vendored
Normal 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"]
|
||||
}
|
4
.github/workflows/cifuzz.yml
vendored
4
.github/workflows/cifuzz.yml
vendored
|
@ -14,6 +14,10 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Fuzzing:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
8
.github/workflows/lint.yml
vendored
8
.github/workflows/lint.yml
vendored
|
@ -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
|
||||
|
|
4
.github/workflows/release-drafter.yml
vendored
4
.github/workflows/release-drafter.yml
vendored
|
@ -10,6 +10,10 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
permissions:
|
||||
|
|
6
.github/workflows/stale.yml
vendored
6
.github/workflows/stale.yml
vendored
|
@ -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"
|
||||
|
|
6
.github/workflows/test-cygwin.yml
vendored
6
.github/workflows/test-cygwin.yml
vendored
|
@ -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'
|
||||
|
||||
|
|
6
.github/workflows/test-docker.yml
vendored
6
.github/workflows/test-docker.yml
vendored
|
@ -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 }}
|
||||
|
|
14
.github/workflows/test-mingw.yml
vendored
14
.github/workflows/test-mingw.yml
vendored
|
@ -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:
|
||||
|
|
4
.github/workflows/test-valgrind.yml
vendored
4
.github/workflows/test-valgrind.yml
vendored
|
@ -16,6 +16,10 @@ on:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
|
|
14
.github/workflows/test-windows.yml
vendored
14
.github/workflows/test-windows.yml
vendored
|
@ -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
|
||||
|
|
20
.github/workflows/test.yml
vendored
20
.github/workflows/test.yml
vendored
|
@ -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:
|
||||
|
|
5
.github/workflows/tidelift.yml
vendored
5
.github/workflows/tidelift.yml
vendored
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
42
CHANGES.rst
42
CHANGES.rst
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
3
Makefile
3
Makefile
|
@ -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
|
||||
|
|
|
@ -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
BIN
Tests/images/bc6h.dds
Normal file
Binary file not shown.
BIN
Tests/images/bc6h.png
Normal file
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
BIN
Tests/images/bc6h_sf.dds
Normal file
Binary file not shown.
BIN
Tests/images/bc6h_sf.png
Normal file
BIN
Tests/images/bc6h_sf.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
Tests/images/bw_gradient.imt
Normal file
BIN
Tests/images/bw_gradient.imt
Normal file
Binary file not shown.
BIN
Tests/images/child_ifd.tiff
Normal file
BIN
Tests/images/child_ifd.tiff
Normal file
Binary file not shown.
BIN
Tests/images/child_ifd_jpeg.tiff
Normal file
BIN
Tests/images/child_ifd_jpeg.tiff
Normal file
Binary file not shown.
BIN
Tests/images/hopper_palette_chunk_second.fli
Normal file
BIN
Tests/images/hopper_palette_chunk_second.fli
Normal file
Binary file not shown.
BIN
Tests/images/text_float_coord.png
Normal file
BIN
Tests/images/text_float_coord.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
Tests/images/text_float_coord_1_alt.png
Normal file
BIN
Tests/images/text_float_coord_1_alt.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 807 B |
Binary file not shown.
|
@ -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))
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
19
Tests/test_file_imt.py
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}",
|
||||
)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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",
|
||||
[
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~
|
||||
|
|
|
@ -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 doesn’t 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 doesn’t 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`
|
||||
|
|
|
@ -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**
|
||||
|
|
|
@ -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
|
||||
========
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
=============
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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))])
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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))])
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"``.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
2
src/thirdparty/raqm/README.md
vendored
2
src/thirdparty/raqm/README.md
vendored
|
@ -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
Loading…
Reference in New Issue
Block a user