Merge branch 'main' into gif

This commit is contained in:
Andrew Murray 2022-03-11 08:44:42 +11:00
commit ca6724bb9d
103 changed files with 1181 additions and 655 deletions

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout Pillow
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up shell
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
@ -45,6 +45,7 @@ jobs:
${{ matrix.package }}-python-pyqt6 \
${{ matrix.package }}-python3-setuptools \
${{ matrix.package }}-freetype \
${{ matrix.package }}-gcc \
${{ matrix.package }}-ghostscript \
${{ matrix.package }}-lcms2 \
${{ matrix.package }}-libimagequant \

View File

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

View File

@ -23,17 +23,17 @@ jobs:
steps:
- name: Checkout Pillow
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Checkout cached dependencies
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: python-pillow/pillow-depends
path: winbuild\depends
# sets env: pythonLocation
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}
@ -137,10 +137,11 @@ jobs:
& $env:pythonLocation\python.exe selftest.py --installed
shell: pwsh
# failing with PyPy3
# skip PyPy for speed
- name: Enable heap verification
if: "!contains(matrix.python-version, 'pypy')"
run: "& 'C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x86\\gflags.exe' /p /enable $env:pythonLocation\\python.exe"
run: |
& reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
- name: Test Pillow
run: |
@ -155,7 +156,7 @@ jobs:
shell: bash
- name: Upload errors
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: failure()
with:
name: errors
@ -181,7 +182,7 @@ jobs:
winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
shell: cmd
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
if: "github.event_name != 'pull_request'"
with:
name: ${{ steps.wheel.outputs.dist }}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: f1d4e742c91dd5179d742b0db9293c4472b765f8 # frozen: 21.12b0
rev: fc0be6eb1e2a96091e6f64009ee5e9081bf8b6c6 # frozen: 22.1.0
hooks:
- id: black
args: ["--target-version", "py37"]
@ -19,7 +19,7 @@ repos:
- id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10
rev: ca52c4245639abd55c970e6bbbca95cab3de22d8 # frozen: v1.1.13
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)

View File

@ -1,2 +1,8 @@
version: 2
python:
pip_install: true
install:
- method: pip
path: .
extra_requirements:
- docs

View File

@ -5,6 +5,27 @@ Changelog (Pillow)
9.1.0 (unreleased)
------------------
- When converting, clip I;16 to be unsigned, not signed #6112
[radarhere]
- Fixed loading L mode GIF with transparency #6086
[radarhere]
- Improved handling of PPM header #5121
[Piolie, radarhere]
- Reset size when seeking away from "Large Thumbnail" MPO frame #6101
[radarhere]
- Replace requirements.txt with extras #6072
[hugovk, radarhere]
- Added PyEncoder and support BLP saving #6069
[radarhere]
- Handle TGA images with packets that cross scan lines #6087
[radarhere]
- Added FITS reading #6056
[radarhere, hugovk]

View File

@ -9,9 +9,11 @@ clean:
.PHONY: coverage
coverage:
pytest -qq
python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
python3 -m pytest -qq
rm -r htmlcov || true
coverage report
python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage
python3 -m coverage report
.PHONY: doc
doc:
@ -33,20 +35,16 @@ help:
@echo "Welcome to Pillow development. Please use \`make <target>\` where <target> is one of"
@echo " clean remove build products"
@echo " coverage run coverage test (in progress)"
@echo " doc make html docs"
@echo " docserve run an http server on the docs directory"
@echo " doc make HTML docs"
@echo " docserve run an HTTP server on the docs directory"
@echo " html to make standalone HTML files"
@echo " inplace make inplace extension"
@echo " install make and install"
@echo " install-coverage make and install with C coverage"
@echo " install-req install documentation and test dependencies"
@echo " install-venv (deprecated) install in virtualenv"
@echo " lint run the lint checks"
@echo " lint-fix run black and isort to (mostly) fix lint issues."
@echo " lint-fix run Black and isort to (mostly) fix lint issues"
@echo " release-test run code and package tests before release"
@echo " test run tests on installed pillow"
@echo " upload build and upload sdists to PyPI"
@echo " upload-test build and upload sdists to test.pythonpackages.com"
@echo " test run tests on installed Pillow"
.PHONY: inplace
inplace: clean
@ -70,28 +68,17 @@ debug:
make clean > /dev/null
CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null
.PHONY: install-req
install-req:
python3 -m pip install -r requirements.txt
.PHONY: install-venv
install-venv:
echo "'install-venv' is deprecated and will be removed in a future Pillow release"
virtualenv .
bin/pip install -r requirements.txt
.PHONY: release-test
release-test:
$(MAKE) install-req
python3 -m pip install -e .
python3 -m pip install -e .[tests]
python3 selftest.py
python3 -m pytest Tests
python3 -m pip install .
-rm dist/*.egg
-rmdir dist
python3 -m pytest -qq
check-manifest
pyroma .
python3 -m check-manifest
python3 -m pyroma .
$(MAKE) readme
.PHONY: sdist
@ -101,26 +88,30 @@ sdist:
.PHONY: test
test:
pytest -qq
python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
python3 -m pytest -qq
.PHONY: valgrind
valgrind:
python3 -c "import pytest_valgrind" || python3 -m pip install pytest-valgrind
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
--log-file=/tmp/valgrind-output \
python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output
.PHONY: readme
readme:
markdown2 README.md > .long-description.html && open .long-description.html
python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2
python3 -m markdown2 README.md > .long-description.html && open .long-description.html
.PHONY: lint
lint:
tox --help > /dev/null || python3 -m pip install tox
tox -e lint
python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox
python3 -m tox -e lint
.PHONY: lint-fix
lint-fix:
black --target-version py37 .
isort .
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort
python3 -m black --target-version py37 .
python3 -m isort .

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 B

View File

@ -1,6 +1,5 @@
import os
import pytest
import warnings
from PIL import Image
@ -20,16 +19,14 @@ def test_bad():
either"""
for f in get_files("b"):
with pytest.warns(None) as record:
# Assert that there is no unclosed file warning
with warnings.catch_warnings():
try:
with Image.open(f) as im:
im.load()
except Exception: # as msg:
pass
# Assert that there is no unclosed file warning
assert not record
def test_questionable():
"""These shouldn't crash/dos, but it's not well defined that these

View File

@ -2,7 +2,12 @@ import pytest
from PIL import BlpImagePlugin, Image
from .helper import assert_image_equal_tofile
from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar,
hopper,
)
def test_load_blp1():
@ -25,6 +30,28 @@ def test_load_blp2_dxt1a():
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
def test_save(tmp_path):
f = str(tmp_path / "temp.blp")
for version in ("BLP1", "BLP2"):
im = hopper("P")
im.save(f, blp_version=version)
with Image.open(f) as reloaded:
assert_image_equal(im.convert("RGB"), reloaded)
with Image.open("Tests/images/transparent.png") as im:
f = str(tmp_path / "temp.blp")
im.convert("P").save(f, blp_version=version)
with Image.open(f) as reloaded:
assert_image_similar(im, reloaded, 8)
im = hopper()
with pytest.raises(ValueError):
im.save(f)
@pytest.mark.parametrize(
"test_file",
[

View File

@ -1,3 +1,5 @@
import warnings
import pytest
from PIL import DcxImagePlugin, Image
@ -31,21 +33,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(TEST_FILE)
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(TEST_FILE) as im:
im.load()
assert not record
def test_invalid_file():
with open("Tests/images/flower.jpg", "rb") as fp:

View File

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

View File

@ -1,3 +1,5 @@
import warnings
import pytest
from PIL import FliImagePlugin, Image
@ -38,21 +40,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(static_test_file)
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(static_test_file) as im:
im.load()
assert not record
def test_tell():
# Arrange

View File

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

View File

@ -1,3 +1,4 @@
import warnings
from io import BytesIO
import pytest
@ -39,21 +40,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(TEST_GIF)
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(TEST_GIF) as im:
im.load()
assert not record
def test_invalid_file():
invalid_file = "Tests/images/flower.jpg"
@ -62,6 +59,17 @@ def test_invalid_file():
GifImagePlugin.GifImageFile(invalid_file)
def test_l_mode_transparency():
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
assert im.mode == "L"
assert im.load()[0, 0] == 0
assert im.info["transparency"] == 255
im.seek(1)
assert im.mode == "LA"
assert im.load()[0, 0] == (0, 255)
def test_optimize():
def test_grayscale(optimize):
im = Image.new("L", (1, 1), 0)
@ -311,6 +319,22 @@ def test_n_frames():
assert im.is_animated == (n_frames != 1)
def test_no_change():
# Test n_frames does not change the image
with Image.open("Tests/images/dispose_bgnd.gif") as im:
im.seek(1)
expected = im.copy()
assert im.n_frames == 5
assert_image_equal(im, expected)
# Test is_animated does not change the image
with Image.open("Tests/images/dispose_bgnd.gif") as im:
im.seek(3)
expected = im.copy()
assert im.is_animated
assert_image_equal(im, expected)
def test_eoferror():
with Image.open(TEST_GIF) as im:
n_frames = im.n_frames

View File

@ -1,5 +1,6 @@
import io
import os
import warnings
import pytest
@ -19,9 +20,8 @@ def test_sanity():
with Image.open(TEST_FILE) as im:
# Assert that there is no unclosed file warning
with pytest.warns(None) as record:
with warnings.catch_warnings():
im.load()
assert not record
assert im.mode == "RGBA"
assert im.size == (1024, 1024)

View File

@ -1,4 +1,5 @@
import filecmp
import warnings
import pytest
@ -35,21 +36,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(TEST_IM)
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(TEST_IM) as im:
im.load()
assert not record
def test_tell():
# Arrange

View File

@ -1,5 +1,6 @@
import os
import re
import warnings
from io import BytesIO
import pytest
@ -756,9 +757,8 @@ class TestFileJpeg:
assert exif[282] == 180
out = str(tmp_path / "out.jpg")
with pytest.warns(None) as record:
with warnings.catch_warnings():
im.save(out, exif=exif)
assert not record
with Image.open(out) as reloaded:
assert reloaded.getexif()[282] == 180

View File

@ -1,3 +1,4 @@
import warnings
from io import BytesIO
import pytest
@ -41,21 +42,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(test_files[0])
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(test_files[0]) as im:
im.load()
assert not record
def test_app():
for test_file in test_files:
@ -88,6 +85,9 @@ def test_frame_size():
im.seek(1)
assert im.size == (680, 480)
im.seek(0)
assert im.size == (640, 480)
def test_ignore_frame_size():
# Ignore the different size of the second frame

View File

@ -1,5 +1,6 @@
import re
import sys
import warnings
import zlib
from io import BytesIO
@ -331,9 +332,8 @@ class TestFilePng:
with Image.open(TEST_PNG_FILE) as im:
# Assert that there is no unclosed file warning
with pytest.warns(None) as record:
with warnings.catch_warnings():
im.verify()
assert not record
with Image.open(TEST_PNG_FILE) as im:
im.load()

View File

@ -3,7 +3,7 @@ from io import BytesIO
import pytest
from PIL import Image
from PIL import Image, UnidentifiedImageError
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
@ -50,15 +50,70 @@ def test_pnm(tmp_path):
assert_image_equal_tofile(im, f)
def test_magic(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"PyInvalid")
with pytest.raises(UnidentifiedImageError):
with Image.open(path):
pass
def test_header_with_comments(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n")
with Image.open(path) as im:
assert im.size == (128, 128)
def test_non_integer_token(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\nTEST")
with pytest.raises(ValueError):
with Image.open(path):
pass
def test_token_too_long(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\n 01234567890")
with pytest.raises(ValueError) as e:
with Image.open(path):
pass
assert str(e.value) == "Token too long in file header: b'01234567890'"
def test_too_many_colors(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\n1 1\n1000\n")
with pytest.raises(ValueError) as e:
with Image.open(path):
pass
assert str(e.value) == "Too many colors for band: 1000"
def test_truncated_file(tmp_path):
path = str(tmp_path / "temp.pgm")
with open(path, "w") as f:
f.write("P6")
with pytest.raises(ValueError):
with pytest.raises(ValueError) as e:
with Image.open(path):
pass
assert str(e.value) == "Reached EOF while reading header"
def test_neg_ppm():
# Storage.c accepted negative values for xsize, ysize. the

View File

@ -1,3 +1,5 @@
import warnings
import pytest
from PIL import Image, PsdImagePlugin
@ -29,21 +31,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(test_file)
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(test_file) as im:
im.load()
assert not record
def test_invalid_file():
invalid_file = "Tests/images/flower.jpg"

View File

@ -1,4 +1,5 @@
import tempfile
import warnings
from io import BytesIO
import pytest
@ -28,21 +29,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(TEST_FILE)
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(TEST_FILE) as im:
im.load()
assert not record
def test_save(tmp_path):
# Arrange

View File

@ -1,3 +1,5 @@
import warnings
import pytest
from PIL import Image, TarIO, features
@ -31,16 +33,12 @@ def test_unclosed_file():
def test_close():
with pytest.warns(None) as record:
with warnings.catch_warnings():
tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg")
tar.close()
assert not record
def test_contextmanager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"):
pass
assert not record

View File

@ -97,6 +97,11 @@ def test_id_field_rle():
assert im.size == (199, 199)
def test_cross_scan_line():
with Image.open("Tests/images/cross_scan_line.tga") as im:
assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png")
def test_save(tmp_path):
test_file = "Tests/images/tga_id_field.tga"
with Image.open(test_file) as im:

View File

@ -1,4 +1,5 @@
import os
import warnings
from io import BytesIO
import pytest
@ -64,20 +65,16 @@ class TestFileTiff:
pytest.warns(ResourceWarning, open)
def test_closed_file(self):
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open("Tests/images/multipage.tiff")
im.load()
im.close()
assert not record
def test_context_manager(self):
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open("Tests/images/multipage.tiff") as im:
im.load()
assert not record
def test_mac_tiff(self):
# Read RGBa images from macOS [@PIL136]

View File

@ -1,6 +1,7 @@
import io
import re
import sys
import warnings
import pytest
@ -161,9 +162,8 @@ class TestFileWebp:
file_path = "Tests/images/hopper.webp"
with Image.open(file_path) as image:
temp_file = str(tmp_path / "temp.webp")
with pytest.warns(None) as record:
with warnings.catch_warnings():
image.save(temp_file)
assert not record
def test_file_pointer_could_be_reused(self):
file_path = "Tests/images/hopper.webp"

View File

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

View File

@ -3,6 +3,7 @@ import os
import shutil
import sys
import tempfile
import warnings
import pytest
@ -648,9 +649,8 @@ class TestImage:
# Act/Assert
with Image.open(test_file) as im:
with pytest.warns(None) as record:
with warnings.catch_warnings():
im.save(temp_file)
assert not record
def test_load_on_nonexclusive_multiframe(self):
with open("Tests/images/frozenpond.mpo", "rb") as fp:

View File

@ -154,14 +154,17 @@ class TestImageGetPixel(AccessTest):
# Check 0
im = Image.new(mode, (0, 0), None)
with pytest.raises(IndexError):
assert im.load() is not None
error = ValueError if self._need_cffi_access else IndexError
with pytest.raises(error):
im.putpixel((0, 0), c)
with pytest.raises(IndexError):
with pytest.raises(error):
im.getpixel((0, 0))
# Check 0 negative index
with pytest.raises(IndexError):
with pytest.raises(error):
im.putpixel((-1, -1), c)
with pytest.raises(IndexError):
with pytest.raises(error):
im.getpixel((-1, -1))
# check initial color
@ -176,10 +179,10 @@ class TestImageGetPixel(AccessTest):
# Check 0
im = Image.new(mode, (0, 0), c)
with pytest.raises(IndexError):
with pytest.raises(error):
im.getpixel((0, 0))
# Check 0 negative index
with pytest.raises(IndexError):
with pytest.raises(error):
im.getpixel((-1, -1))
def test_basic(self):

View File

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

View File

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

View File

@ -124,6 +124,23 @@ class TestImageFile:
with pytest.raises(OSError):
p.close()
def test_no_format(self):
buf = BytesIO(b"\x00" * 255)
class DummyImageFile(ImageFile.ImageFile):
def _open(self):
self.mode = "RGB"
self._size = (1, 1)
im = DummyImageFile(buf)
assert im.format is None
assert im.get_format_mimetype() is None
def test_oserror(self):
im = Image.new("RGB", (1, 1))
with pytest.raises(OSError):
im.save(BytesIO(), "JPEG2000", num_resolutions=2)
def test_truncated(self):
b = BytesIO(
b"BM000000000000" # head_data
@ -179,6 +196,14 @@ class MockPyDecoder(ImageFile.PyDecoder):
return -1, 0
class MockPyEncoder(ImageFile.PyEncoder):
def encode(self, buffer):
return 1, 1, b""
def cleanup(self):
self.cleanup_called = True
xoff, yoff, xsize, ysize = 10, 20, 100, 100
@ -190,53 +215,58 @@ class MockImageFile(ImageFile.ImageFile):
self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)]
class TestPyDecoder:
def get_decoder(self):
decoder = MockPyDecoder(None)
class CodecsTest:
@classmethod
def setup_class(cls):
cls.decoder = MockPyDecoder(None)
cls.encoder = MockPyEncoder(None)
def closure(mode, *args):
decoder.__init__(mode, *args)
return decoder
def decoder_closure(mode, *args):
cls.decoder.__init__(mode, *args)
return cls.decoder
Image.register_decoder("MOCK", closure)
return decoder
def encoder_closure(mode, *args):
cls.encoder.__init__(mode, *args)
return cls.encoder
Image.register_decoder("MOCK", decoder_closure)
Image.register_encoder("MOCK", encoder_closure)
class TestPyDecoder(CodecsTest):
def test_setimage(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
d = self.get_decoder()
im.load()
assert d.state.xoff == xoff
assert d.state.yoff == yoff
assert d.state.xsize == xsize
assert d.state.ysize == ysize
assert self.decoder.state.xoff == xoff
assert self.decoder.state.yoff == yoff
assert self.decoder.state.xsize == xsize
assert self.decoder.state.ysize == ysize
with pytest.raises(ValueError):
d.set_as_raw(b"\x00")
self.decoder.set_as_raw(b"\x00")
def test_extents_none(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)]
d = self.get_decoder()
im.load()
assert d.state.xoff == 0
assert d.state.yoff == 0
assert d.state.xsize == 200
assert d.state.ysize == 200
assert self.decoder.state.xoff == 0
assert self.decoder.state.yoff == 0
assert self.decoder.state.xsize == 200
assert self.decoder.state.ysize == 200
def test_negsize(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
self.get_decoder()
with pytest.raises(ValueError):
im.load()
@ -250,7 +280,6 @@ class TestPyDecoder:
im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)]
self.get_decoder()
with pytest.raises(ValueError):
im.load()
@ -259,14 +288,92 @@ class TestPyDecoder:
with pytest.raises(ValueError):
im.load()
def test_no_format(self):
def test_decode(self):
decoder = ImageFile.PyDecoder(None)
with pytest.raises(NotImplementedError):
decoder.decode(None)
class TestPyEncoder(CodecsTest):
def test_setimage(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
assert im.format is None
assert im.get_format_mimetype() is None
def test_oserror(self):
im = Image.new("RGB", (1, 1))
with pytest.raises(OSError):
im.save(BytesIO(), "JPEG2000", num_resolutions=2)
fp = BytesIO()
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
)
assert self.encoder.state.xoff == xoff
assert self.encoder.state.yoff == yoff
assert self.encoder.state.xsize == xsize
assert self.encoder.state.ysize == ysize
def test_extents_none(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)]
fp = BytesIO()
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")])
assert self.encoder.state.xoff == 0
assert self.encoder.state.yoff == 0
assert self.encoder.state.xsize == 200
assert self.encoder.state.ysize == 200
def test_negsize(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
fp = BytesIO()
self.encoder.cleanup_called = False
with pytest.raises(ValueError):
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
)
assert self.encoder.cleanup_called
with pytest.raises(ValueError):
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")]
)
def test_oversize(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
fp = BytesIO()
with pytest.raises(ValueError):
ImageFile._save(
im,
fp,
[("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")],
)
with pytest.raises(ValueError):
ImageFile._save(
im,
fp,
[("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")],
)
def test_encode(self):
encoder = ImageFile.PyEncoder(None)
with pytest.raises(NotImplementedError):
encoder.encode(None)
bytes_consumed, errcode = encoder.encode_to_pyfd()
assert bytes_consumed == 0
assert ImageFile.ERRORS[errcode] == "bad configuration"
encoder._pushes_fd = True
with pytest.raises(NotImplementedError):
encoder.encode_to_pyfd()
with pytest.raises(NotImplementedError):
encoder.encode_to_file(None, None)

View File

@ -88,19 +88,6 @@ class TestImageFont:
ImageFont.truetype(tempfile, FONT_SIZE)
def test_unavailable_layout_engine(self):
have_raqm = ImageFont.core.HAVE_RAQM
ImageFont.core.HAVE_RAQM = False
try:
ttf = ImageFont.truetype(
FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM
)
finally:
ImageFont.core.HAVE_RAQM = have_raqm
assert ttf.layout_engine == ImageFont.Layout.BASIC
def _render(self, font):
txt = "Hello World!"
ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE)

View File

@ -1,3 +1,5 @@
import warnings
import pytest
from PIL import ImageQt
@ -56,7 +58,5 @@ def test_image():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
ImageQt.ImageQt("Tests/images/hopper.gif")
assert not record

View File

@ -1,3 +1,5 @@
import warnings
import pytest
from PIL import Image
@ -237,6 +239,5 @@ def test_no_resource_warning_for_numpy_array():
with Image.open(test_file) as im:
# Act/Assert
with pytest.warns(None) as record:
with warnings.catch_warnings():
array(im)
assert not record

View File

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

View File

@ -3,7 +3,7 @@
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXBUILD = python3 -m sphinx.cmd.build
PAPER =
BUILDDIR = _build
@ -41,38 +41,48 @@ help:
clean:
-rm -rf $(BUILDDIR)/*
install-sphinx:
python3 -c "import sphinx" > /dev/null 2>&1 || python3 -m pip install sphinx
html:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
@ -82,6 +92,7 @@ qthelp:
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc"
devhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@ -91,11 +102,13 @@ devhelp:
@echo "# devhelp"
epub:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@ -103,22 +116,26 @@ latex:
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@ -126,28 +143,33 @@ texinfo:
"(use \`make info' here to do that automatically)."
info:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

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

View File

@ -8,4 +8,4 @@ Appendices
image-file-formats
text-anchors
writing-your-own-file-decoder
writing-your-own-image-plugin

View File

@ -26,6 +26,20 @@ Fully supported formats
.. contents::
BLP
^^^
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.
Pillow supports writing BLP images. The :py:meth:`~PIL.Image.Image.save` method
can take the following keyword arguments:
**blp_version**
If present and set to "BLP1", images will be saved as BLP1. Otherwise, images
will be saved as BLP2.
BMP
^^^
@ -1042,13 +1056,6 @@ Pillow reads and writes X bitmap files (mode ``1``).
Read-only formats
-----------------
BLP
^^^
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.
CUR
^^^

View File

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

View File

@ -465,6 +465,8 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Fedora 35 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 10.15 Catalina | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 |

View File

@ -40,8 +40,16 @@ Classes
.. autoclass:: PIL.ImageFile.Parser()
:members:
.. autoclass:: PIL.ImageFile.PyCodec()
:members:
.. autoclass:: PIL.ImageFile.PyDecoder()
:members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.PyEncoder()
:members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.ImageFile()
:member-order: bysource

View File

@ -23,6 +23,9 @@ All default viewers convert the image to be shown to PNG format.
.. autoclass:: PIL.ImageShow.EogViewer
.. autoclass:: PIL.ImageShow.XVViewer
To provide maximum functionality on Unix-based systems, temporary files created
from images will not be automatically removed by Pillow.
.. autofunction:: PIL.ImageShow.register
.. autoclass:: PIL.ImageShow.Viewer
:member-order: bysource

View File

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

View File

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

View File

@ -10,6 +10,10 @@ The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the
looping over all of the pixels in an image, there is likely
a faster way using other parts of the Pillow API.
:mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps`
have methods for many standard operations. If you wish to perform
a custom mapping, check out :py:meth:`~PIL.Image.Image.point`.
Example
-------

View File

@ -53,7 +53,7 @@ Then ``sudo apt-get update && sudo apt-get install libtiff5-dbgsym``
virtualenv -p python3.8-dbg ~/vpy38-dbg
source ~/vpy38-dbg/bin/activate
cd ~/Pillow && pip install -r requirements.txt && make install
cd ~/Pillow && make install
Test Case
---------

View File

@ -18,11 +18,37 @@ Rather than returning a ``SystemError``, passing the incorrect types of coordina
a path will now raise a more specific ``ValueError``, with the message "incorrect
coordinate type".
Replace requirements.txt with extras
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Rather than installing all dependencies for docs and tests via ``requirements.txt``,
``extras_require`` is used instead. This installs only those needed and at the same
time as installing Pillow.
For example:
.. code-block:: bash
# Install with dependencies for tests:
python3 -m pip install .[tests]
# Or for building docs:
python3 -m pip install .[docs]
# Or for all:
python3 -m pip install .[docs,tests]
On macOS, the last argument may need to be wrapped in quotes, e.g.
``python3 -m pip install ".[tests]"``
Therefore ``requirements.txt`` has been removed along with the ``make install-req``
command for installing its contents.
Deprecations
^^^^^^^^^^^^
============
Constants
~~~~~~~~~
^^^^^^^^^
A number of constants have been deprecated and will be removed in Pillow 10.0.0
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
@ -87,7 +113,7 @@ Deprecated Use instead
===================================================== ============================================================
ImageShow.Viewer.show_file file argument
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been
deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by
@ -98,7 +124,7 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
``viewer.show_file(path="test.jpg")`` instead.
FitsStubImagePlugin
~~~~~~~~~~~~~~~~~~~
^^^^^^^^^^^^^^^^^^^
.. deprecated:: 9.1.0
@ -127,6 +153,13 @@ By default, :py:meth:`~PIL.Image.Image.getpalette` returns RGB data from the pal
A ``rawmode`` argument has been added, to allow the mode to be chosen instead. ``None``
can be used to return data in the current mode of the palette.
Added PyEncoder
^^^^^^^^^^^^^^^
:py:class:`~PIL.ImageFile.PyEncoder` has been added, allowing for file encoders to be
written in Python. See :ref:`Writing Your Own File Codec in Python<file-codecs-py>` for
more information.
Other Changes
=============
@ -143,3 +176,10 @@ Image._repr_pretty_
``im._repr_pretty_`` has been added to provide a representation of an image without the
identity of the object. This allows Jupyter to describe an image and have that
description stay the same on subsequent executions of the same code.
Added BLP saving
^^^^^^^^^^^^^^^^
Support has been added for saving BLP images. ``blp_version`` can be used to specify
whether the image should be saved as BLP1 or BLP2, e.g.
``im.save("out.blp", blp_version="BLP1")``. By default, BLP2 will be used.

View File

@ -1,18 +0,0 @@
# Development, documentation & testing requirements.
black
check-manifest
coverage
defusedxml
markdown2
olefile
packaging
pyroma
pytest
pytest-cov
pytest-timeout
sphinx>=2.4
sphinx-copybutton
sphinx-issues>=3.0.1
sphinx-removed-in
sphinx-rtd-theme>=1.0
sphinxext-opengraph

View File

@ -35,6 +35,27 @@ project_urls =
[options]
python_requires = >=3.7
[options.extras_require]
docs =
olefile
sphinx>=2.4
sphinx-copybutton
sphinx-issues>=3.0.1
sphinx-removed-in
sphinx-rtd-theme>=1.0
sphinxext-opengraph
tests =
check-manifest
coverage
defusedxml
markdown2
olefile
packaging
pyroma
pytest
pytest-cov
pytest-timeout
[flake8]
extend-ignore = E203
max-line-length = 88

View File

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

View File

@ -29,6 +29,7 @@ BLP files come in many different flavours:
- DXT5 compression is used if alpha_encoding == 7.
"""
import os
import struct
import warnings
from enum import IntEnum
@ -266,6 +267,10 @@ class BLPFormatError(NotImplementedError):
pass
def _accept(prefix):
return prefix[:4] in (b"BLP1", b"BLP2")
class BlpImageFile(ImageFile.ImageFile):
"""
Blizzard Mipmap Format
@ -276,50 +281,51 @@ class BlpImageFile(ImageFile.ImageFile):
def _open(self):
self.magic = self.fp.read(4)
self._read_blp_header()
if self.magic == b"BLP1":
decoder = "BLP1"
self.mode = "RGB"
elif self.magic == b"BLP2":
decoder = "BLP2"
self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.fp.seek(5, os.SEEK_CUR)
(self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
self.fp.seek(2, os.SEEK_CUR)
self._size = struct.unpack("<II", self.fp.read(8))
if self.magic in (b"BLP1", b"BLP2"):
decoder = self.magic.decode()
else:
raise BLPFormatError(f"Bad BLP magic {repr(self.magic)}")
self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
def _read_blp_header(self):
(self._blp_compression,) = struct.unpack("<i", self.fp.read(4))
(self._blp_encoding,) = struct.unpack("<b", self.fp.read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self.fp.read(1))
(self._blp_mips,) = struct.unpack("<b", self.fp.read(1))
self._size = struct.unpack("<II", self.fp.read(8))
if self.magic == b"BLP1":
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self.fp.read(4))
(self._blp_subtype,) = struct.unpack("<i", self.fp.read(4))
self._blp_offsets = struct.unpack("<16I", self.fp.read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self.fp.read(16 * 4))
class _BLPBaseDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
try:
self.fd.seek(0)
self.magic = self.fd.read(4)
self._read_blp_header()
self._load()
except struct.error as e:
raise OSError("Truncated Blp file") from e
return 0, 0
raise OSError("Truncated BLP file") from e
return -1, 0
def _read_blp_header(self):
self.fd.seek(4)
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1))
self.fd.seek(1, os.SEEK_CUR) # mips
self.size = struct.unpack("<II", self._safe_read(8))
if isinstance(self, BLP1Decoder):
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
self.fd.seek(4, os.SEEK_CUR) # subtype
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length):
return ImageFile._safe_read(self.fd, length)
@ -334,23 +340,20 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
ret.append((b, g, r, a))
return ret
def _read_blp_header(self):
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1))
(self._blp_mips,) = struct.unpack("<b", self._safe_read(1))
self.size = struct.unpack("<II", self._safe_read(8))
if self.magic == b"BLP1":
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
(self._blp_subtype,) = struct.unpack("<i", self._safe_read(4))
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _read_bgra(self, palette):
data = bytearray()
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
d = (r, g, b)
if self._blp_alpha_depth:
d += (a,)
data.extend(d)
return data
class BLP1Decoder(_BLPBaseDecoder):
@ -360,17 +363,8 @@ class BLP1Decoder(_BLPBaseDecoder):
elif self._blp_compression == 1:
if self._blp_encoding in (4, 5):
data = bytearray()
palette = self._read_palette()
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
data.extend([r, g, b])
data = self._read_bgra(palette)
self.set_as_raw(bytes(data))
else:
raise BLPFormatError(
@ -401,23 +395,16 @@ class BLP2Decoder(_BLPBaseDecoder):
def _load(self):
palette = self._read_palette()
data = bytearray()
self.fd.seek(self._blp_offsets[0])
if self._blp_compression == 1:
# Uncompressed or DirectX compression
if self._blp_encoding == Encoding.UNCOMPRESSED:
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
data.extend((r, g, b))
data = self._read_bgra(palette)
elif self._blp_encoding == Encoding.DXT:
data = bytearray()
if self._blp_alpha_encoding == AlphaEncoding.DXT1:
linesize = (self.size[0] + 3) // 4 * 8
for yb in range((self.size[1] + 3) // 4):
@ -452,12 +439,59 @@ class BLP2Decoder(_BLPBaseDecoder):
self.set_as_raw(bytes(data))
def _accept(prefix):
return prefix[:4] in (b"BLP1", b"BLP2")
class BLPEncoder(ImageFile.PyEncoder):
_pushes_fd = True
def _write_palette(self):
data = b""
palette = self.im.getpalette("RGBA", "RGBA")
for i in range(256):
r, g, b, a = palette[i * 4 : (i + 1) * 4]
data += struct.pack("<4B", b, g, r, a)
return data
def encode(self, bufsize):
palette_data = self._write_palette()
offset = 20 + 16 * 4 * 2 + len(palette_data)
data = struct.pack("<16I", offset, *((0,) * 15))
w, h = self.im.size
data += struct.pack("<16I", w * h, *((0,) * 15))
data += palette_data
for y in range(h):
for x in range(w):
data += struct.pack("<B", self.im.getpixel((x, y)))
return len(data), 0, data
def _save(im, fp, filename, save_all=False):
if im.mode != "P":
raise ValueError("Unsupported BLP image mode")
magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2"
fp.write(magic)
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
fp.write(struct.pack("<b", 1 if im.palette.mode == "RGBA" else 0))
fp.write(struct.pack("<b", 0)) # alpha encoding
fp.write(struct.pack("<b", 0)) # mips
fp.write(struct.pack("<II", *im.size))
if magic == b"BLP1":
fp.write(struct.pack("<i", 5))
fp.write(struct.pack("<i", 0))
ImageFile._save(im, fp, [("BLP", (0, 0) + im.size, 0, im.mode)])
Image.register_open(BlpImageFile.format, BlpImageFile, _accept)
Image.register_extension(BlpImageFile.format, ".blp")
Image.register_decoder("BLP1", BLP1Decoder)
Image.register_decoder("BLP2", BLP2Decoder)
Image.register_save(BlpImageFile.format, _save)
Image.register_encoder("BLP", BLPEncoder)

View File

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

View File

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

View File

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

View File

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

View File

@ -175,9 +175,15 @@ class GifImageFile(ImageFile.ImageFile):
if self.__frame == 1:
self.pyaccess = None
if "transparency" in self.info:
self.mode = "RGBA"
if self.mode == "P":
self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
self.mode = "RGBA"
else:
self.im = self.im.convert_transparent(
"LA", self.info["transparency"]
)
self.mode = "LA"
del self.info["transparency"]
else:
@ -315,7 +321,7 @@ class GifImageFile(ImageFile.ImageFile):
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
else:
# replace with previous contents
if self.im:
if self.im is not None:
# only dispose the extent in this frame
self.dispose = self._crop(self.im, self.dispose_extent)
elif frame_transparency is not None:
@ -382,15 +388,18 @@ class GifImageFile(ImageFile.ImageFile):
if self.__frame == 0:
return
if self._frame_transparency is not None:
if self.mode == "P":
self.im.putpalettealpha(self._frame_transparency, 0)
frame_im = self.im.convert("RGBA")
else:
frame_im = self.im.convert_transparent("LA", self._frame_transparency)
else:
frame_im = self.im.convert("RGB")
frame_im = self._crop(frame_im, self.dispose_extent)
self.im = self._prev_im
self.mode = self.im.mode
if frame_im.mode == "RGBA":
if frame_im.mode in ("LA", "RGBA"):
self.im.paste(frame_im, self.dispose_extent, frame_im)
else:
self.im.paste(frame_im, self.dispose_extent)

View File

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

View File

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

View File

@ -304,7 +304,7 @@ class IcoImageFile(ImageFile.ImageFile):
self._size = value
def load(self):
if self.im and self.im.size == self.size:
if self.im is not None and self.im.size == self.size:
# Already loaded
return Image.Image.load(self)
im = self.ico.getimage(self.size)

View File

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

View File

@ -847,7 +847,7 @@ class Image:
:returns: An image access object.
:rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess`
"""
if self.im and self.palette and self.palette.dirty:
if self.im is not None and self.palette and self.palette.dirty:
# realize palette
mode, arr = self.palette.getdata()
self.im.putpalette(mode, arr)
@ -864,7 +864,7 @@ class Image:
self.palette.mode = palette_mode
self.palette.palette = self.im.getpalette(palette_mode, palette_mode)
if self.im:
if self.im is not None:
if cffi and USE_CFFI_ACCESS:
if self.pyaccess:
return self.pyaccess
@ -975,7 +975,9 @@ class Image:
delete_trns = False
# transparency handling
if has_transparency:
if self.mode in ("1", "L", "I", "RGB") and mode == "RGBA":
if (self.mode in ("1", "L", "I") and mode in ("LA", "RGBA")) or (
self.mode == "RGB" and mode == "RGBA"
):
# Use transparent conversion to promote from transparent
# color to an alpha channel.
new_im = self._new(
@ -1492,11 +1494,12 @@ class Image:
def histogram(self, mask=None, extrema=None):
"""
Returns a histogram for the image. The histogram is returned as
a list of pixel counts, one for each pixel value in the source
image. If the image has more than one band, the histograms for
all bands are concatenated (for example, the histogram for an
"RGB" image contains 768 values).
Returns a histogram for the image. The histogram is returned as a
list of pixel counts, one for each pixel value in the source
image. Counts are grouped into 256 bins for each band, even if
the image has more than 8 bits per band. If the image has more
than one band, the histograms for all bands are concatenated (for
example, the histogram for an "RGB" image contains 768 values).
A bilevel image (mode "1") is treated as a greyscale ("L") image
by this method.
@ -1564,8 +1567,8 @@ class Image:
also use color strings as supported by the ImageColor module.
If a mask is given, this method updates only the regions
indicated by the mask. You can use either "1", "L" or "RGBA"
images (in the latter case, the alpha band is used as mask).
indicated by the mask. You can use either "1", "L", "LA", "RGBA"
or "RGBa" images (if present, the alpha band is used as mask).
Where the mask is 255, the given image is copied as is. Where
the mask is 0, the current value is preserved. Intermediate
values will mix the two images together, including their alpha
@ -1613,7 +1616,7 @@ class Image:
elif isImageType(im):
im.load()
if self.mode != im.mode:
if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"):
if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"):
# should use an adapter for this!
im = im.convert(self.mode)
im = im.im
@ -2781,7 +2784,7 @@ def frombytes(mode, size, data, decoder_name="raw", *args):
You can also use any pixel decoder supported by PIL. For more
information on available decoders, see the section
:ref:`Writing Your Own File Decoder <file-decoders>`.
:ref:`Writing Your Own File Codec <file-codecs>`.
Note that this function decodes pixel data only, not entire images.
If you have an entire image in a string, wrap it in a

View File

@ -49,7 +49,11 @@ ERRORS = {
-8: "bad configuration",
-9: "out of memory error",
}
"""Dict of known error codes returned from :meth:`.PyDecoder.decode`."""
"""
Dict of known error codes returned from :meth:`.PyDecoder.decode`,
:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and
:meth:`.PyEncoder.encode_to_file`.
"""
#
@ -219,15 +223,15 @@ class ImageFile(Image.Image):
)
]
for decoder_name, extents, offset, args in self.tile:
seek(offset)
decoder = Image._getdecoder(
self.mode, decoder_name, args, self.decoderconfig
)
try:
seek(offset)
decoder.setimage(self.im, extents)
if decoder.pulls_fd:
decoder.setfd(self.fp)
status, err_code = decoder.decode(b"")
err_code = decoder.decode(b"")[1]
else:
b = prefix
while True:
@ -495,40 +499,33 @@ def _save(im, fp, tile, bufsize=0):
try:
fh = fp.fileno()
fp.flush()
except (AttributeError, io.UnsupportedOperation) as exc:
# compress to Python file-compatible object
exc = None
except (AttributeError, io.UnsupportedOperation) as e:
exc = e
for e, b, o, a in tile:
e = Image._getencoder(im.mode, e, a, im.encoderconfig)
if o > 0:
fp.seek(o)
e.setimage(im.im, b)
if e.pushes_fd:
e.setfd(fp)
l, s = e.encode_to_pyfd()
encoder = Image._getencoder(im.mode, e, a, im.encoderconfig)
try:
encoder.setimage(im.im, b)
if encoder.pushes_fd:
encoder.setfd(fp)
l, s = encoder.encode_to_pyfd()
else:
if exc:
# compress to Python file-compatible object
while True:
l, s, d = e.encode(bufsize)
l, s, d = encoder.encode(bufsize)
fp.write(d)
if s:
break
if s < 0:
raise OSError(f"encoder error {s} when writing image file") from exc
e.cleanup()
else:
# slight speedup: compress to real file object
for e, b, o, a in tile:
e = Image._getencoder(im.mode, e, a, im.encoderconfig)
if o > 0:
fp.seek(o)
e.setimage(im.im, b)
if e.pushes_fd:
e.setfd(fp)
l, s = e.encode_to_pyfd()
else:
s = e.encode_to_file(fh, bufsize)
s = encoder.encode_to_file(fh, bufsize)
if s < 0:
raise OSError(f"encoder error {s} when writing image file")
e.cleanup()
raise OSError(f"encoder error {s} when writing image file") from exc
finally:
encoder.cleanup()
if hasattr(fp, "flush"):
fp.flush()
@ -577,16 +574,7 @@ class PyCodecState:
return (self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize)
class PyDecoder:
"""
Python implementation of a format decoder. Override this class and
add the decoding logic in the :meth:`decode` method.
See :ref:`Writing Your Own File Decoder in Python<file-decoders-py>`
"""
_pulls_fd = False
class PyCodec:
def __init__(self, mode, *args):
self.im = None
self.state = PyCodecState()
@ -596,31 +584,16 @@ class PyDecoder:
def init(self, args):
"""
Override to perform decoder specific initialization
Override to perform codec specific initialization
:param args: Array of args items from the tile entry
:returns: None
"""
self.args = args
@property
def pulls_fd(self):
return self._pulls_fd
def decode(self, buffer):
"""
Override to perform the decoding process.
:param buffer: A bytes object with the data to be decoded.
:returns: A tuple of ``(bytes consumed, errcode)``.
If finished with decoding return <0 for the bytes consumed.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
raise NotImplementedError()
def cleanup(self):
"""
Override to perform decoder specific cleanup
Override to perform codec specific cleanup
:returns: None
"""
@ -628,16 +601,16 @@ class PyDecoder:
def setfd(self, fd):
"""
Called from ImageFile to set the python file-like object
Called from ImageFile to set the Python file-like object
:param fd: A python file-like object
:param fd: A Python file-like object
:returns: None
"""
self.fd = fd
def setimage(self, im, extents=None):
"""
Called from ImageFile to set the core output image for the decoder
Called from ImageFile to set the core output image for the codec
:param im: A core image object
:param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle
@ -670,6 +643,32 @@ class PyDecoder:
):
raise ValueError("Tile cannot extend outside image")
class PyDecoder(PyCodec):
"""
Python implementation of a format decoder. Override this class and
add the decoding logic in the :meth:`decode` method.
See :ref:`Writing Your Own File Codec in Python<file-codecs-py>`
"""
_pulls_fd = False
@property
def pulls_fd(self):
return self._pulls_fd
def decode(self, buffer):
"""
Override to perform the decoding process.
:param buffer: A bytes object with the data to be decoded.
:returns: A tuple of ``(bytes consumed, errcode)``.
If finished with decoding return -1 for the bytes consumed.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
raise NotImplementedError()
def set_as_raw(self, data, rawmode=None):
"""
Convenience method to set the internal image from a stream of raw data
@ -690,3 +689,60 @@ class PyDecoder:
raise ValueError("not enough image data")
if s[1] != 0:
raise ValueError("cannot decode image data")
class PyEncoder(PyCodec):
"""
Python implementation of a format encoder. Override this class and
add the decoding logic in the :meth:`encode` method.
See :ref:`Writing Your Own File Codec in Python<file-codecs-py>`
"""
_pushes_fd = False
@property
def pushes_fd(self):
return self._pushes_fd
def encode(self, bufsize):
"""
Override to perform the encoding process.
:param bufsize: Buffer size.
:returns: A tuple of ``(bytes encoded, errcode, bytes)``.
If finished with encoding return 1 for the error code.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
raise NotImplementedError()
def encode_to_pyfd(self):
"""
If ``pushes_fd`` is ``True``, then this method will be used,
and ``encode()`` will only be called once.
:returns: A tuple of ``(bytes consumed, errcode)``.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
if not self.pushes_fd:
return 0, -8 # bad configuration
bytes_consumed, errcode, data = self.encode(0)
if data:
self.fd.write(data)
return bytes_consumed, errcode
def encode_to_file(self, fh, bufsize):
"""
:param fh: File handle.
:param bufsize: Buffer size.
:returns: If finished successfully, return 0.
Otherwise, return an error code. Err codes are from
:data:`.ImageFile.ERRORS`.
"""
errcode = 0
while errcode == 0:
status, errcode, buf = self.encode(bufsize)
if status > 0:
fh.write(buf[status:])
return errcode

View File

@ -839,7 +839,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
:file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on
macOS.
:param size: The requested size, in points.
:param size: The requested size, in pixels.
:param index: Which font face to load (default is first available face).
:param encoding: Which font encoding to use (default is Unicode). Possible
encodings include (see the FreeType documentation for more

View File

@ -25,7 +25,12 @@ _viewers = []
def register(viewer, order=1):
"""
The :py:func:`register` function is used to register additional viewers.
The :py:func:`register` function is used to register additional viewers::
from PIL import ImageShow
ImageShow.register(MyViewer()) # MyViewer will be used as a last resort
ImageShow.register(MySecondViewer(), 0) # MySecondViewer will be prioritised
ImageShow.register(ImageShow.XVViewer(), 0) # XVViewer will be prioritised
:param viewer: The viewer to be registered.
:param order:

View File

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

View File

@ -46,6 +46,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
self._after_jpeg_open()
def _after_jpeg_open(self, mpheader=None):
self._initial_size = self.size
self.mpinfo = mpheader if mpheader is not None else self._getmp()
self.n_frames = self.mpinfo[0xB001]
self.__mpoffsets = [
@ -77,6 +78,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
segment = self.fp.read(2)
if not segment:
raise ValueError("No data found for frame")
self._size = self._initial_size
if i16(segment) == 0xFFE1: # APP1
n = i16(self.fp.read(2)) - 2
self.info["exif"] = ImageFile._safe_read(self.fp, n)

View File

@ -148,7 +148,7 @@ class MspDecoder(ImageFile.PyDecoder):
self.set_as_raw(img.getvalue(), ("1", 0, 1))
return 0, 0
return -1, 0
Image.register_decoder("MSP", MspDecoder)

View File

@ -576,42 +576,42 @@ class PdfParser:
self.xref_table[reference.object_id] = (offset, 0)
return reference
delimiter = br"[][()<>{}/%]"
delimiter_or_ws = br"[][()<>{}/%\000\011\012\014\015\040]"
whitespace = br"[\000\011\012\014\015\040]"
whitespace_or_hex = br"[\000\011\012\014\015\0400-9a-fA-F]"
delimiter = rb"[][()<>{}/%]"
delimiter_or_ws = rb"[][()<>{}/%\000\011\012\014\015\040]"
whitespace = rb"[\000\011\012\014\015\040]"
whitespace_or_hex = rb"[\000\011\012\014\015\0400-9a-fA-F]"
whitespace_optional = whitespace + b"*"
whitespace_mandatory = whitespace + b"+"
# No "\012" aka "\n" or "\015" aka "\r":
whitespace_optional_no_nl = br"[\000\011\014\040]*"
newline_only = br"[\r\n]+"
whitespace_optional_no_nl = rb"[\000\011\014\040]*"
newline_only = rb"[\r\n]+"
newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl
re_trailer_end = re.compile(
whitespace_mandatory
+ br"trailer"
+ rb"trailer"
+ whitespace_optional
+ br"\<\<(.*\>\>)"
+ rb"\<\<(.*\>\>)"
+ newline
+ br"startxref"
+ rb"startxref"
+ newline
+ br"([0-9]+)"
+ rb"([0-9]+)"
+ newline
+ br"%%EOF"
+ rb"%%EOF"
+ whitespace_optional
+ br"$",
+ rb"$",
re.DOTALL,
)
re_trailer_prev = re.compile(
whitespace_optional
+ br"trailer"
+ rb"trailer"
+ whitespace_optional
+ br"\<\<(.*?\>\>)"
+ rb"\<\<(.*?\>\>)"
+ newline
+ br"startxref"
+ rb"startxref"
+ newline
+ br"([0-9]+)"
+ rb"([0-9]+)"
+ newline
+ br"%%EOF"
+ rb"%%EOF"
+ whitespace_optional,
re.DOTALL,
)
@ -655,12 +655,12 @@ class PdfParser:
re_whitespace_optional = re.compile(whitespace_optional)
re_name = re.compile(
whitespace_optional
+ br"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?="
+ rb"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?="
+ delimiter_or_ws
+ br")"
+ rb")"
)
re_dict_start = re.compile(whitespace_optional + br"\<\<")
re_dict_end = re.compile(whitespace_optional + br"\>\>" + whitespace_optional)
re_dict_start = re.compile(whitespace_optional + rb"\<\<")
re_dict_end = re.compile(whitespace_optional + rb"\>\>" + whitespace_optional)
@classmethod
def interpret_trailer(cls, trailer_data):
@ -689,7 +689,7 @@ class PdfParser:
)
return trailer
re_hashes_in_name = re.compile(br"([^#]*)(#([0-9a-fA-F]{2}))?")
re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?")
@classmethod
def interpret_name(cls, raw, as_text=False):
@ -704,53 +704,53 @@ class PdfParser:
else:
return bytes(name)
re_null = re.compile(whitespace_optional + br"null(?=" + delimiter_or_ws + br")")
re_true = re.compile(whitespace_optional + br"true(?=" + delimiter_or_ws + br")")
re_false = re.compile(whitespace_optional + br"false(?=" + delimiter_or_ws + br")")
re_null = re.compile(whitespace_optional + rb"null(?=" + delimiter_or_ws + rb")")
re_true = re.compile(whitespace_optional + rb"true(?=" + delimiter_or_ws + rb")")
re_false = re.compile(whitespace_optional + rb"false(?=" + delimiter_or_ws + rb")")
re_int = re.compile(
whitespace_optional + br"([-+]?[0-9]+)(?=" + delimiter_or_ws + br")"
whitespace_optional + rb"([-+]?[0-9]+)(?=" + delimiter_or_ws + rb")"
)
re_real = re.compile(
whitespace_optional
+ br"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?="
+ rb"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?="
+ delimiter_or_ws
+ br")"
+ rb")"
)
re_array_start = re.compile(whitespace_optional + br"\[")
re_array_end = re.compile(whitespace_optional + br"]")
re_array_start = re.compile(whitespace_optional + rb"\[")
re_array_end = re.compile(whitespace_optional + rb"]")
re_string_hex = re.compile(
whitespace_optional + br"\<(" + whitespace_or_hex + br"*)\>"
whitespace_optional + rb"\<(" + whitespace_or_hex + rb"*)\>"
)
re_string_lit = re.compile(whitespace_optional + br"\(")
re_string_lit = re.compile(whitespace_optional + rb"\(")
re_indirect_reference = re.compile(
whitespace_optional
+ br"([-+]?[0-9]+)"
+ rb"([-+]?[0-9]+)"
+ whitespace_mandatory
+ br"([-+]?[0-9]+)"
+ rb"([-+]?[0-9]+)"
+ whitespace_mandatory
+ br"R(?="
+ rb"R(?="
+ delimiter_or_ws
+ br")"
+ rb")"
)
re_indirect_def_start = re.compile(
whitespace_optional
+ br"([-+]?[0-9]+)"
+ rb"([-+]?[0-9]+)"
+ whitespace_mandatory
+ br"([-+]?[0-9]+)"
+ rb"([-+]?[0-9]+)"
+ whitespace_mandatory
+ br"obj(?="
+ rb"obj(?="
+ delimiter_or_ws
+ br")"
+ rb")"
)
re_indirect_def_end = re.compile(
whitespace_optional + br"endobj(?=" + delimiter_or_ws + br")"
whitespace_optional + rb"endobj(?=" + delimiter_or_ws + rb")"
)
re_comment = re.compile(
br"(" + whitespace_optional + br"%[^\r\n]*" + newline + br")*"
rb"(" + whitespace_optional + rb"%[^\r\n]*" + newline + rb")*"
)
re_stream_start = re.compile(whitespace_optional + br"stream\r?\n")
re_stream_start = re.compile(whitespace_optional + rb"stream\r?\n")
re_stream_end = re.compile(
whitespace_optional + br"endstream(?=" + delimiter_or_ws + br")"
whitespace_optional + rb"endstream(?=" + delimiter_or_ws + rb")"
)
@classmethod
@ -876,7 +876,7 @@ class PdfParser:
raise PdfFormatError("unrecognized object: " + repr(data[offset : offset + 32]))
re_lit_str_token = re.compile(
br"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))"
rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))"
)
escaped_chars = {
b"n": b"\n",
@ -922,16 +922,16 @@ class PdfParser:
offset = m.end()
raise PdfFormatError("unfinished literal string")
re_xref_section_start = re.compile(whitespace_optional + br"xref" + newline)
re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline)
re_xref_subsection_start = re.compile(
whitespace_optional
+ br"([0-9]+)"
+ rb"([0-9]+)"
+ whitespace_mandatory
+ br"([0-9]+)"
+ rb"([0-9]+)"
+ whitespace_optional
+ newline_only
)
re_xref_entry = re.compile(br"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)")
re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)")
def read_xref_table(self, xref_section_offset):
subsection_found = False

View File

@ -48,7 +48,7 @@ from ._binary import o32be as o32
logger = logging.getLogger(__name__)
is_cid = re.compile(br"\w\w\w\w").match
is_cid = re.compile(rb"\w\w\w\w").match
_MAGIC = b"\211PNG\r\n\032\n"

View File

@ -49,26 +49,46 @@ class PpmImageFile(ImageFile.ImageFile):
format = "PPM"
format_description = "Pbmplus image"
def _token(self, s=b""):
while True: # read until next whitespace
def _read_magic(self):
magic = b""
# read until whitespace or longest available magic number
for _ in range(6):
c = self.fp.read(1)
if not c or c in b_whitespace:
break
if c > b"\x79":
raise ValueError("Expected ASCII value, found binary")
s = s + c
if len(s) > 9:
raise ValueError("Expected int, got > 9 digits")
return s
magic += c
return magic
def _read_token(self):
token = b""
while len(token) <= 10: # read until next whitespace or limit of 10 characters
c = self.fp.read(1)
if not c:
break
elif c in b_whitespace: # token ended
if not token:
# skip whitespace at start
continue
break
elif c == b"#":
# ignores rest of the line; stops at CR, LF or EOF
while self.fp.read(1) not in b"\r\n":
pass
continue
token += c
if not token:
# Token was not even 1 byte
raise ValueError("Reached EOF while reading header")
elif len(token) > 10:
raise ValueError(f"Token too long in file header: {token}")
return token
def _open(self):
# check magic
s = self.fp.read(1)
if s != b"P":
raise SyntaxError("not a PPM file")
magic_number = self._token(s)
magic_number = self._read_magic()
try:
mode = MODES[magic_number]
except KeyError:
raise SyntaxError("not a PPM file")
self.custom_mimetype = {
b"P4": "image/x-portable-bitmap",
@ -83,29 +103,19 @@ class PpmImageFile(ImageFile.ImageFile):
self.mode = rawmode = mode
for ix in range(3):
while True:
while True:
s = self.fp.read(1)
if s not in b_whitespace:
break
if s == b"":
raise ValueError("File does not extend beyond magic number")
if s != b"#":
break
s = self.fp.readline()
s = int(self._token(s))
if ix == 0:
xsize = s
elif ix == 1:
ysize = s
token = int(self._read_token())
if ix == 0: # token is the x size
xsize = token
elif ix == 1: # token is the y size
ysize = token
if mode == "1":
break
elif ix == 2:
# maxgrey
if s > 255:
elif ix == 2: # token is maxval
maxval = token
if maxval > 255:
if not mode == "L":
raise ValueError(f"Too many colors for band: {s}")
if s < 2 ** 16:
raise ValueError(f"Too many colors for band: {token}")
if maxval < 2**16:
self.mode = "I"
rawmode = "I;16B"
else:

View File

@ -155,14 +155,6 @@ class PsdImageFile(ImageFile.ImageFile):
# return layer number (0=image, 1..max=layers)
return self.frame
def load_prepare(self):
# create image memory if necessary
if not self.im or self.im.mode != self.mode or self.im.size != self.size:
self.im = Image.core.fill(self.mode, self.size, 0)
# create palette (optional)
if self.mode == "P":
Image.Image.load(self)
def _close__fp(self):
try:
if self.__fp != self.fp:

View File

@ -493,7 +493,7 @@ class ImageFileDirectory_v2(MutableMapping):
endianness.
:param prefix: Override the endianness of the file.
"""
if ifh[:4] not in PREFIXES:
if not _accept(ifh):
raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)")
self._prefix = prefix if prefix is not None else ifh[:2]
if self._prefix == MM:

View File

@ -21,7 +21,6 @@
from . import Image, ImageFile
from ._binary import i16le as word
from ._binary import i32le as dword
from ._binary import si16le as short
from ._binary import si32le as _long
@ -112,7 +111,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
if s[22:26] != b"\x01\x00\t\x00":
raise SyntaxError("Unsupported WMF file format")
elif dword(s) == 1 and s[40:44] == b" EMF":
elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF":
# enhanced metafile
# get bounding box

View File

@ -25,7 +25,7 @@ from . import Image, ImageFile
# XBM header
xbm_head = re.compile(
br"\s*#define[ \t]+.*_width[ \t]+(?P<width>[0-9]+)[\r\n]+"
rb"\s*#define[ \t]+.*_width[ \t]+(?P<width>[0-9]+)[\r\n]+"
b"#define[ \t]+.*_height[ \t]+(?P<height>[0-9]+)[\r\n]+"
b"(?P<hotspot>"
b"#define[ \t]+[^_]*_x_hot[ \t]+(?P<xhot>[0-9]+)[\r\n]+"
@ -52,7 +52,8 @@ class XbmImageFile(ImageFile.ImageFile):
m = xbm_head.match(self.fp.read(512))
if m:
if not m:
raise SyntaxError("not a XBM file")
xsize = int(m.group("width"))
ysize = int(m.group("height"))

View File

@ -149,14 +149,13 @@ _encode(ImagingEncoderObject *encoder, PyObject *args) {
}
static PyObject *
_encode_to_pyfd(ImagingEncoderObject *encoder, PyObject *args) {
_encode_to_pyfd(ImagingEncoderObject *encoder) {
PyObject *result;
int status;
if (!encoder->pushes_fd) {
// UNDONE, appropriate errcode???
result = Py_BuildValue("ii", 0, IMAGING_CODEC_CONFIG);
;
return result;
}
@ -307,7 +306,7 @@ static struct PyMethodDef methods[] = {
{"encode", (PyCFunction)_encode, METH_VARARGS},
{"cleanup", (PyCFunction)_encode_cleanup, METH_VARARGS},
{"encode_to_file", (PyCFunction)_encode_to_file, METH_VARARGS},
{"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_VARARGS},
{"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_NOARGS},
{"setimage", (PyCFunction)_setimage, METH_VARARGS},
{"setfd", (PyCFunction)_setfd, METH_VARARGS},
{NULL, NULL} /* sentinel */

View File

@ -37,7 +37,7 @@
#define MAX(a, b) (a) > (b) ? (a) : (b)
#define MIN(a, b) (a) < (b) ? (a) : (b)
#define CLIP16(v) ((v) <= -32768 ? -32768 : (v) >= 32767 ? 32767 : (v))
#define CLIP16(v) ((v) <= 0 ? 0 : (v) >= 65535 ? 65535 : (v))
/* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */
#define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114)
@ -1634,29 +1634,15 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) {
return (Imaging)ImagingError_ModeError();
}
if (!((strcmp(imIn->mode, "RGB") == 0 || strcmp(imIn->mode, "1") == 0 ||
strcmp(imIn->mode, "I") == 0 || strcmp(imIn->mode, "L") == 0) &&
strcmp(mode, "RGBA") == 0))
#ifdef notdef
{
return (Imaging)ImagingError_ValueError("conversion not supported");
}
#else
{
static char buf[100];
snprintf(
buf,
100,
"conversion from %.10s to %.10s not supported in convert_transparent",
imIn->mode,
mode);
return (Imaging)ImagingError_ValueError(buf);
}
#endif
if (strcmp(imIn->mode, "RGB") == 0) {
if (strcmp(imIn->mode, "RGB") == 0 && strcmp(mode, "RGBA") == 0) {
convert = rgb2rgba;
} else {
} else if ((strcmp(imIn->mode, "1") == 0 ||
strcmp(imIn->mode, "I") == 0 ||
strcmp(imIn->mode, "L") == 0
) && (
strcmp(mode, "RGBA") == 0 ||
strcmp(mode, "LA") == 0
)) {
if (strcmp(imIn->mode, "1") == 0) {
convert = bit2rgb;
} else if (strcmp(imIn->mode, "I") == 0) {
@ -1665,6 +1651,15 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) {
convert = l2rgb;
}
g = b = r;
} else {
static char buf[100];
snprintf(
buf,
100,
"conversion from %.10s to %.10s not supported in convert_transparent",
imIn->mode,
mode);
return (Imaging)ImagingError_ValueError(buf);
}
imOut = ImagingNew2Dirty(mode, imOut, imIn);

View File

@ -295,7 +295,7 @@ ImagingPaste(
paste_mask_L(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize);
ImagingSectionLeave(&cookie);
} else if (strcmp(imMask->mode, "RGBA") == 0) {
} else if (strcmp(imMask->mode, "LA") == 0 || strcmp(imMask->mode, "RGBA") == 0) {
ImagingSectionEnter(&cookie);
paste_mask_RGBA(
imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize);

View File

@ -20,6 +20,8 @@ int
ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) {
int n, depth;
UINT8 *ptr;
UINT8 extra_data = 0;
int extra_bytes = 0;
ptr = buf;
@ -42,15 +44,13 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t
return ptr - buf;
}
n = depth * ((ptr[0] & 0x7f) + 1);
if (ptr[0] & 0x80) {
/* Run (1 + pixelsize bytes) */
if (bytes < 1 + depth) {
break;
}
n = depth * ((ptr[0] & 0x7f) + 1);
if (state->x + n > state->bytes) {
state->errcode = IMAGING_CODEC_OVERRUN;
return -1;
@ -67,18 +67,17 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t
ptr += 1 + depth;
bytes -= 1 + depth;
} else {
/* Literal (1+n+1 bytes block) */
n = depth * (ptr[0] + 1);
if (bytes < 1 + n) {
break;
}
if (state->x + n > state->bytes) {
state->errcode = IMAGING_CODEC_OVERRUN;
return -1;
extra_bytes = n; /* full value */
n = state->bytes - state->x;
extra_bytes -= n;
extra_data = ptr[1];
}
memcpy(state->buffer + state->x, ptr + 1, n);
@ -87,6 +86,7 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t
bytes -= 1 + n;
}
for (;;) {
state->x += n;
if (state->x >= state->bytes) {
@ -106,6 +106,24 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t
return -1;
}
}
if (extra_bytes == 0) {
break;
}
if (state->x > 0) {
break; // assert
}
if (extra_bytes >= state->bytes) {
n = state->bytes;
} else {
n = extra_bytes;
}
memcpy(state->buffer + state->x, ptr, n);
ptr += n;
extra_bytes -= n;
}
}
return ptr - buf;

View File

@ -10,6 +10,8 @@ envlist =
minversion = 1.9
[testenv]
extras =
tests
commands =
make clean
{envpython} -m pip install --global-option="build_ext" --global-option="--inplace" .
@ -18,9 +20,6 @@ commands =
deps =
cffi
numpy
olefile
pyroma
pytest
[testenv:lint]
commands =

View File

@ -107,9 +107,9 @@ header = [
# dependencies, listed in order of compilation
deps = {
"libjpeg": {
"url": SF_MIRROR + "/project/libjpeg-turbo/2.1.2/libjpeg-turbo-2.1.2.tar.gz",
"filename": "libjpeg-turbo-2.1.2.tar.gz",
"dir": "libjpeg-turbo-2.1.2",
"url": SF_MIRROR + "/project/libjpeg-turbo/2.1.3/libjpeg-turbo-2.1.3.tar.gz",
"filename": "libjpeg-turbo-2.1.3.tar.gz",
"dir": "libjpeg-turbo-2.1.3",
"build": [
cmd_cmake(
[
@ -280,9 +280,9 @@ deps = {
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/3.4.0.zip",
"filename": "harfbuzz-3.4.0.zip",
"dir": "harfbuzz-3.4.0",
"url": "https://github.com/harfbuzz/harfbuzz/archive/4.0.0.zip",
"filename": "harfbuzz-4.0.0.zip",
"dir": "harfbuzz-4.0.0",
"build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
cmd_nmake(target="clean"),
@ -464,7 +464,7 @@ def build_dep_all():
if dep_name in disabled:
continue
script = build_dep(dep_name)
lines.append(fr'cmd.exe /c "{{build_dir}}\{script}"')
lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"')
lines.append("if errorlevel 1 echo Build failed! && exit /B 1")
lines.append("@echo All Pillow dependencies built successfully!")
write_script("build_dep_all.cmd", lines)