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 language: python
dry-run: false dry-run: false
- name: Upload New Crash - name: Upload New Crash
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: failure() && steps.build.outcome == 'success' if: failure() && steps.build.outcome == 'success'
with: with:
name: artifacts name: artifacts
path: ./out/artifacts path: ./out/artifacts
- name: Upload Legacy Crash - name: Upload Legacy Crash
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: steps.run.outcome == 'success' if: steps.run.outcome == 'success'
with: with:
name: crash name: crash

View File

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

View File

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

View File

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

View File

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

View File

@ -23,17 +23,17 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Checkout cached dependencies - name: Checkout cached dependencies
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
repository: python-pillow/pillow-depends repository: python-pillow/pillow-depends
path: winbuild\depends path: winbuild\depends
# sets env: pythonLocation # sets env: pythonLocation
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v3
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }} architecture: ${{ matrix.architecture }}
@ -137,10 +137,11 @@ jobs:
& $env:pythonLocation\python.exe selftest.py --installed & $env:pythonLocation\python.exe selftest.py --installed
shell: pwsh shell: pwsh
# failing with PyPy3 # skip PyPy for speed
- name: Enable heap verification - name: Enable heap verification
if: "!contains(matrix.python-version, 'pypy')" 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 - name: Test Pillow
run: | run: |
@ -155,7 +156,7 @@ jobs:
shell: bash shell: bash
- name: Upload errors - name: Upload errors
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: failure() if: failure()
with: with:
name: errors name: errors
@ -181,7 +182,7 @@ jobs:
winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
shell: cmd shell: cmd
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v3
if: "github.event_name != 'pull_request'" if: "github.event_name != 'pull_request'"
with: with:
name: ${{ steps.wheel.outputs.dist }} name: ${{ steps.wheel.outputs.dist }}

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,27 @@ Changelog (Pillow)
9.1.0 (unreleased) 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 - Added FITS reading #6056
[radarhere, hugovk] [radarhere, hugovk]

View File

@ -9,9 +9,11 @@ clean:
.PHONY: coverage .PHONY: coverage
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 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 .PHONY: doc
doc: doc:
@ -33,20 +35,16 @@ help:
@echo "Welcome to Pillow development. Please use \`make <target>\` where <target> is one of" @echo "Welcome to Pillow development. Please use \`make <target>\` where <target> is one of"
@echo " clean remove build products" @echo " clean remove build products"
@echo " coverage run coverage test (in progress)" @echo " coverage run coverage test (in progress)"
@echo " doc make html docs" @echo " doc make HTML docs"
@echo " docserve run an http server on the docs directory" @echo " docserve run an HTTP server on the docs directory"
@echo " html to make standalone HTML files" @echo " html to make standalone HTML files"
@echo " inplace make inplace extension" @echo " inplace make inplace extension"
@echo " install make and install" @echo " install make and install"
@echo " install-coverage make and install with C coverage" @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 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 " release-test run code and package tests before release"
@echo " test run tests on installed pillow" @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"
.PHONY: inplace .PHONY: inplace
inplace: clean inplace: clean
@ -70,28 +68,17 @@ debug:
make clean > /dev/null make clean > /dev/null
CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /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 .PHONY: release-test
release-test: release-test:
$(MAKE) install-req python3 -m pip install -e .[tests]
python3 -m pip install -e .
python3 selftest.py python3 selftest.py
python3 -m pytest Tests python3 -m pytest Tests
python3 -m pip install . python3 -m pip install .
-rm dist/*.egg -rm dist/*.egg
-rmdir dist -rmdir dist
python3 -m pytest -qq python3 -m pytest -qq
check-manifest python3 -m check-manifest
pyroma . python3 -m pyroma .
$(MAKE) readme $(MAKE) readme
.PHONY: sdist .PHONY: sdist
@ -101,26 +88,30 @@ sdist:
.PHONY: test .PHONY: test
test: test:
pytest -qq python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
python3 -m pytest -qq
.PHONY: valgrind .PHONY: valgrind
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 \ PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
--log-file=/tmp/valgrind-output \ --log-file=/tmp/valgrind-output \
python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output
.PHONY: readme .PHONY: readme
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 .PHONY: lint
lint: lint:
tox --help > /dev/null || python3 -m pip install tox python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox
tox -e lint python3 -m tox -e lint
.PHONY: lint-fix .PHONY: lint-fix
lint-fix: lint-fix:
black --target-version py37 . python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
isort . python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort
python3 -m black --target-version py37 .
python3 -m isort .

View File

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

View File

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

View File

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

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

View File

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

View File

@ -2,7 +2,12 @@ import pytest
from PIL import BlpImagePlugin, Image 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(): def test_load_blp1():
@ -25,6 +30,28 @@ def test_load_blp2_dxt1a():
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") 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( @pytest.mark.parametrize(
"test_file", "test_file",
[ [

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import warnings
from io import BytesIO from io import BytesIO
import pytest import pytest
@ -39,21 +40,17 @@ def test_unclosed_file():
def test_closed_file(): def test_closed_file():
with pytest.warns(None) as record: with warnings.catch_warnings():
im = Image.open(TEST_GIF) im = Image.open(TEST_GIF)
im.load() im.load()
im.close() im.close()
assert not record
def test_context_manager(): def test_context_manager():
with pytest.warns(None) as record: with warnings.catch_warnings():
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
im.load() im.load()
assert not record
def test_invalid_file(): def test_invalid_file():
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
@ -62,6 +59,17 @@ def test_invalid_file():
GifImagePlugin.GifImageFile(invalid_file) GifImagePlugin.GifImageFile(invalid_file)
def test_l_mode_transparency():
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
assert im.mode == "L"
assert im.load()[0, 0] == 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_optimize():
def test_grayscale(optimize): def test_grayscale(optimize):
im = Image.new("L", (1, 1), 0) im = Image.new("L", (1, 1), 0)
@ -311,6 +319,22 @@ def test_n_frames():
assert im.is_animated == (n_frames != 1) 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(): def test_eoferror():
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
n_frames = im.n_frames n_frames = im.n_frames

View File

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

View File

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

View File

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

View File

@ -218,7 +218,7 @@ class TestFileLibTiff(LibTiffTestCase):
values = { values = {
2: "test", 2: "test",
3: 1, 3: 1,
4: 2 ** 20, 4: 2**20,
5: TiffImagePlugin.IFDRational(100, 1), 5: TiffImagePlugin.IFDRational(100, 1),
12: 1.05, 12: 1.05,
} }
@ -1019,7 +1019,7 @@ class TestFileLibTiff(LibTiffTestCase):
im = hopper("RGB").resize((256, 256)) im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
TiffImagePlugin.STRIP_SIZE = 2 ** 18 TiffImagePlugin.STRIP_SIZE = 2**18
try: try:
im.save(out, compression="tiff_adobe_deflate") im.save(out, compression="tiff_adobe_deflate")

View File

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

View File

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

View File

@ -3,7 +3,7 @@ from io import BytesIO
import pytest import pytest
from PIL import Image from PIL import Image, UnidentifiedImageError
from .helper import assert_image_equal_tofile, assert_image_similar, hopper 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) 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): def test_truncated_file(tmp_path):
path = str(tmp_path / "temp.pgm") path = str(tmp_path / "temp.pgm")
with open(path, "w") as f: with open(path, "w") as f:
f.write("P6") f.write("P6")
with pytest.raises(ValueError): with pytest.raises(ValueError) as e:
with Image.open(path): with Image.open(path):
pass pass
assert str(e.value) == "Reached EOF while reading header"
def test_neg_ppm(): def test_neg_ppm():
# Storage.c accepted negative values for xsize, ysize. the # Storage.c accepted negative values for xsize, ysize. the

View File

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

View File

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

View File

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

View File

@ -97,6 +97,11 @@ def test_id_field_rle():
assert im.size == (199, 199) 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): def test_save(tmp_path):
test_file = "Tests/images/tga_id_field.tga" test_file = "Tests/images/tga_id_field.tga"
with Image.open(test_file) as im: with Image.open(test_file) as im:

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import io import io
import re import re
import sys import sys
import warnings
import pytest import pytest
@ -127,7 +128,7 @@ class TestFileWebp:
self._roundtrip(tmp_path, "P", 50.0) self._roundtrip(tmp_path, "P", 50.0)
@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_message(self, tmp_path): def test_write_encoding_error_message(self, tmp_path):
temp_file = str(tmp_path / "temp.webp") temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (15000, 15000)) im = Image.new("RGB", (15000, 15000))
@ -161,9 +162,8 @@ class TestFileWebp:
file_path = "Tests/images/hopper.webp" file_path = "Tests/images/hopper.webp"
with Image.open(file_path) as image: with Image.open(file_path) as image:
temp_file = str(tmp_path / "temp.webp") temp_file = str(tmp_path / "temp.webp")
with pytest.warns(None) as record: with warnings.catch_warnings():
image.save(temp_file) image.save(temp_file)
assert not record
def test_file_pointer_could_be_reused(self): def test_file_pointer_could_be_reused(self):
file_path = "Tests/images/hopper.webp" file_path = "Tests/images/hopper.webp"

View File

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

View File

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

View File

@ -154,14 +154,17 @@ class TestImageGetPixel(AccessTest):
# Check 0 # Check 0
im = Image.new(mode, (0, 0), None) im = Image.new(mode, (0, 0), None)
with pytest.raises(IndexError): assert im.load() is not None
error = ValueError if self._need_cffi_access else IndexError
with pytest.raises(error):
im.putpixel((0, 0), c) im.putpixel((0, 0), c)
with pytest.raises(IndexError): with pytest.raises(error):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check 0 negative index # Check 0 negative index
with pytest.raises(IndexError): with pytest.raises(error):
im.putpixel((-1, -1), c) im.putpixel((-1, -1), c)
with pytest.raises(IndexError): with pytest.raises(error):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
# check initial color # check initial color
@ -176,10 +179,10 @@ class TestImageGetPixel(AccessTest):
# Check 0 # Check 0
im = Image.new(mode, (0, 0), c) im = Image.new(mode, (0, 0), c)
with pytest.raises(IndexError): with pytest.raises(error):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check 0 negative index # Check 0 negative index
with pytest.raises(IndexError): with pytest.raises(error):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
def test_basic(self): def test_basic(self):
@ -205,10 +208,10 @@ class TestImageGetPixel(AccessTest):
# see https://github.com/python-pillow/Pillow/issues/452 # see https://github.com/python-pillow/Pillow/issues/452
# pixelaccess is using signed int* instead of uint* # pixelaccess is using signed int* instead of uint*
for mode in ("I;16", "I;16B"): for mode in ("I;16", "I;16B"):
self.check(mode, 2 ** 15 - 1) self.check(mode, 2**15 - 1)
self.check(mode, 2 ** 15) self.check(mode, 2**15)
self.check(mode, 2 ** 15 + 1) self.check(mode, 2**15 + 1)
self.check(mode, 2 ** 16 - 1) self.check(mode, 2**16 - 1)
def test_p_putpixel_rgb_rgba(self): def test_p_putpixel_rgb_rgba(self):
for color in [(255, 0, 0), (255, 0, 0, 255)]: for color in [(255, 0, 0), (255, 0, 0, 255)]:
@ -386,7 +389,7 @@ class TestImagePutPixelError(AccessTest):
def test_putpixel_overflow_error(self, mode): def test_putpixel_overflow_error(self, mode):
im = hopper(mode) im = hopper(mode)
with pytest.raises(OverflowError): with pytest.raises(OverflowError):
im.putpixel((0, 0), 2 ** 80) im.putpixel((0, 0), 2**80)
def test_putpixel_unrecognized_mode(self): def test_putpixel_unrecognized_mode(self):
im = hopper("BGR;15") im = hopper("BGR;15")

View File

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

View File

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

View File

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

View File

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

View File

@ -124,6 +124,23 @@ class TestImageFile:
with pytest.raises(OSError): with pytest.raises(OSError):
p.close() 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): def test_truncated(self):
b = BytesIO( b = BytesIO(
b"BM000000000000" # head_data b"BM000000000000" # head_data
@ -179,6 +196,14 @@ class MockPyDecoder(ImageFile.PyDecoder):
return -1, 0 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 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)] self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)]
class TestPyDecoder: class CodecsTest:
def get_decoder(self): @classmethod
decoder = MockPyDecoder(None) def setup_class(cls):
cls.decoder = MockPyDecoder(None)
cls.encoder = MockPyEncoder(None)
def closure(mode, *args): def decoder_closure(mode, *args):
decoder.__init__(mode, *args) cls.decoder.__init__(mode, *args)
return decoder return cls.decoder
Image.register_decoder("MOCK", closure) def encoder_closure(mode, *args):
return decoder 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): def test_setimage(self):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
d = self.get_decoder()
im.load() im.load()
assert d.state.xoff == xoff assert self.decoder.state.xoff == xoff
assert d.state.yoff == yoff assert self.decoder.state.yoff == yoff
assert d.state.xsize == xsize assert self.decoder.state.xsize == xsize
assert d.state.ysize == ysize assert self.decoder.state.ysize == ysize
with pytest.raises(ValueError): with pytest.raises(ValueError):
d.set_as_raw(b"\x00") self.decoder.set_as_raw(b"\x00")
def test_extents_none(self): def test_extents_none(self):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)] im.tile = [("MOCK", None, 32, None)]
d = self.get_decoder()
im.load() im.load()
assert d.state.xoff == 0 assert self.decoder.state.xoff == 0
assert d.state.yoff == 0 assert self.decoder.state.yoff == 0
assert d.state.xsize == 200 assert self.decoder.state.xsize == 200
assert d.state.ysize == 200 assert self.decoder.state.ysize == 200
def test_negsize(self): def test_negsize(self):
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
self.get_decoder()
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() im.load()
@ -250,7 +280,6 @@ class TestPyDecoder:
im = MockImageFile(buf) im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)]
self.get_decoder()
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() im.load()
@ -259,14 +288,92 @@ class TestPyDecoder:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.load() 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) buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf) im = MockImageFile(buf)
assert im.format is None
assert im.get_format_mimetype() is None
def test_oserror(self): fp = BytesIO()
im = Image.new("RGB", (1, 1)) ImageFile._save(
with pytest.raises(OSError): im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
im.save(BytesIO(), "JPEG2000", num_resolutions=2) )
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) 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): def _render(self, font):
txt = "Hello World!" txt = "Hello World!"
ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE) ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE)

View File

@ -1,3 +1,5 @@
import warnings
import pytest import pytest
from PIL import ImageQt from PIL import ImageQt
@ -30,10 +32,10 @@ def test_rgb():
def checkrgb(r, g, b): def checkrgb(r, g, b):
val = ImageQt.rgb(r, g, b) val = ImageQt.rgb(r, g, b)
val = val % 2 ** 24 # drop the alpha val = val % 2**24 # drop the alpha
assert val >> 16 == r assert val >> 16 == r
assert ((val >> 8) % 2 ** 8) == g assert ((val >> 8) % 2**8) == g
assert val % 2 ** 8 == b assert val % 2**8 == b
checkrgb(0, 0, 0) checkrgb(0, 0, 0)
checkrgb(255, 0, 0) checkrgb(255, 0, 0)
@ -56,7 +58,5 @@ def test_image():
def test_closed_file(): def test_closed_file():
with pytest.warns(None) as record: with warnings.catch_warnings():
ImageQt.ImageQt("Tests/images/hopper.gif") ImageQt.ImageQt("Tests/images/hopper.gif")
assert not record

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,20 @@ Fully supported formats
.. contents:: .. 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 BMP
^^^ ^^^
@ -1042,13 +1056,6 @@ Pillow reads and writes X bitmap files (mode ``1``).
Read-only formats 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 CUR
^^^ ^^^

View File

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

View File

@ -40,8 +40,16 @@ Classes
.. autoclass:: PIL.ImageFile.Parser() .. autoclass:: PIL.ImageFile.Parser()
:members: :members:
.. autoclass:: PIL.ImageFile.PyCodec()
:members:
.. autoclass:: PIL.ImageFile.PyDecoder() .. autoclass:: PIL.ImageFile.PyDecoder()
:members: :members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.PyEncoder()
:members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.ImageFile() .. autoclass:: PIL.ImageFile.ImageFile()
:member-order: bysource :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.EogViewer
.. autoclass:: PIL.ImageShow.XVViewer .. 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 .. autofunction:: PIL.ImageShow.register
.. autoclass:: PIL.ImageShow.Viewer .. autoclass:: PIL.ImageShow.Viewer
:member-order: bysource :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. statistics. You can also pass in a previously calculated histogram.
:param image: A PIL image, or a precalculated histogram. :param image: A PIL image, or a precalculated histogram.
.. note::
For a PIL image, calculations rely on the
:py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are
grouped into 256 bins, even if the image has more than 8 bits per
channel. So ``I`` and ``F`` mode images have a maximum ``mean``,
``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum
of more than 255.
:param mask: An optional mask. :param mask: An optional mask.
.. py:attribute:: extrema .. py:attribute:: extrema

View File

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

View File

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

View File

@ -53,7 +53,7 @@ Then ``sudo apt-get update && sudo apt-get install libtiff5-dbgsym``
virtualenv -p python3.8-dbg ~/vpy38-dbg virtualenv -p python3.8-dbg ~/vpy38-dbg
source ~/vpy38-dbg/bin/activate source ~/vpy38-dbg/bin/activate
cd ~/Pillow && pip install -r requirements.txt && make install cd ~/Pillow && make install
Test Case 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 a path will now raise a more specific ``ValueError``, with the message "incorrect
coordinate type". 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 Deprecations
^^^^^^^^^^^^ ============
Constants Constants
~~~~~~~~~ ^^^^^^^^^
A number of constants have been deprecated and will be removed in Pillow 10.0.0 A number of constants have been deprecated and will be removed in Pillow 10.0.0
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. (2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
@ -87,7 +113,7 @@ Deprecated Use instead
===================================================== ============================================================ ===================================================== ============================================================
ImageShow.Viewer.show_file file argument ImageShow.Viewer.show_file file argument
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been 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 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. ``viewer.show_file(path="test.jpg")`` instead.
FitsStubImagePlugin FitsStubImagePlugin
~~~~~~~~~~~~~~~~~~~ ^^^^^^^^^^^^^^^^^^^
.. deprecated:: 9.1.0 .. 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`` 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. 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 Other Changes
============= =============
@ -143,3 +176,10 @@ Image._repr_pretty_
``im._repr_pretty_`` has been added to provide a representation of an image without the ``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 identity of the object. This allows Jupyter to describe an image and have that
description stay the same on subsequent executions of the same code. description stay the same on subsequent executions of the same code.
Added 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] [options]
python_requires = >=3.7 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] [flake8]
extend-ignore = E203 extend-ignore = E203
max-line-length = 88 max-line-length = 88

View File

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

View File

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

View File

@ -102,7 +102,7 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["height"] = ( file_info["height"] = (
i32(header_data, 4) i32(header_data, 4)
if not file_info["y_flip"] if not file_info["y_flip"]
else 2 ** 32 - i32(header_data, 4) else 2**32 - i32(header_data, 4)
) )
file_info["planes"] = i16(header_data, 8) file_info["planes"] = i16(header_data, 8)
file_info["bits"] = i16(header_data, 10) file_info["bits"] = i16(header_data, 10)
@ -322,7 +322,7 @@ def _save(im, fp, filename, bitmap_header=True):
if bitmap_header: if bitmap_header:
offset = 14 + header + colors * 4 offset = 14 + header + colors * 4
file_size = offset + image file_size = offset + image
if file_size > 2 ** 32 - 1: if file_size > 2**32 - 1:
raise ValueError("File size is too large for the BMP format") raise ValueError("File size is too large for the BMP format")
fp.write( fp.write(
b"BM" # file type (magic) b"BM" # file type (magic)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,7 +49,11 @@ ERRORS = {
-8: "bad configuration", -8: "bad configuration",
-9: "out of memory error", -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: for decoder_name, extents, offset, args in self.tile:
seek(offset)
decoder = Image._getdecoder( decoder = Image._getdecoder(
self.mode, decoder_name, args, self.decoderconfig self.mode, decoder_name, args, self.decoderconfig
) )
try: try:
seek(offset)
decoder.setimage(self.im, extents) decoder.setimage(self.im, extents)
if decoder.pulls_fd: if decoder.pulls_fd:
decoder.setfd(self.fp) decoder.setfd(self.fp)
status, err_code = decoder.decode(b"") err_code = decoder.decode(b"")[1]
else: else:
b = prefix b = prefix
while True: while True:
@ -495,40 +499,33 @@ def _save(im, fp, tile, bufsize=0):
try: try:
fh = fp.fileno() fh = fp.fileno()
fp.flush() fp.flush()
except (AttributeError, io.UnsupportedOperation) as exc: exc = None
# compress to Python file-compatible object except (AttributeError, io.UnsupportedOperation) as e:
for e, b, o, a in tile: exc = e
e = Image._getencoder(im.mode, e, a, im.encoderconfig) for e, b, o, a in tile:
if o > 0: if o > 0:
fp.seek(o) fp.seek(o)
e.setimage(im.im, b) encoder = Image._getencoder(im.mode, e, a, im.encoderconfig)
if e.pushes_fd: try:
e.setfd(fp) encoder.setimage(im.im, b)
l, s = e.encode_to_pyfd() if encoder.pushes_fd:
encoder.setfd(fp)
l, s = encoder.encode_to_pyfd()
else: else:
while True: if exc:
l, s, d = e.encode(bufsize) # compress to Python file-compatible object
fp.write(d) while True:
if s: l, s, d = encoder.encode(bufsize)
break fp.write(d)
if s:
break
else:
# slight speedup: compress to real file object
s = encoder.encode_to_file(fh, bufsize)
if s < 0: if s < 0:
raise OSError(f"encoder error {s} when writing image file") from exc raise OSError(f"encoder error {s} when writing image file") from exc
e.cleanup() finally:
else: encoder.cleanup()
# slight speedup: compress to real file object
for e, b, o, a in tile:
e = Image._getencoder(im.mode, e, a, im.encoderconfig)
if o > 0:
fp.seek(o)
e.setimage(im.im, b)
if e.pushes_fd:
e.setfd(fp)
l, s = e.encode_to_pyfd()
else:
s = e.encode_to_file(fh, bufsize)
if s < 0:
raise OSError(f"encoder error {s} when writing image file")
e.cleanup()
if hasattr(fp, "flush"): if hasattr(fp, "flush"):
fp.flush() fp.flush()
@ -577,16 +574,7 @@ class PyCodecState:
return (self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize) return (self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize)
class PyDecoder: class 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 Decoder in Python<file-decoders-py>`
"""
_pulls_fd = False
def __init__(self, mode, *args): def __init__(self, mode, *args):
self.im = None self.im = None
self.state = PyCodecState() self.state = PyCodecState()
@ -596,31 +584,16 @@ class PyDecoder:
def init(self, args): 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 :param args: Array of args items from the tile entry
:returns: None :returns: None
""" """
self.args = args 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): def cleanup(self):
""" """
Override to perform decoder specific cleanup Override to perform codec specific cleanup
:returns: None :returns: None
""" """
@ -628,16 +601,16 @@ class PyDecoder:
def setfd(self, fd): 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 :returns: None
""" """
self.fd = fd self.fd = fd
def setimage(self, im, extents=None): 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 im: A core image object
:param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle :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") 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): def set_as_raw(self, data, rawmode=None):
""" """
Convenience method to set the internal image from a stream of raw data 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") raise ValueError("not enough image data")
if s[1] != 0: if s[1] != 0:
raise ValueError("cannot decode image data") 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 :file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on
macOS. 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 index: Which font face to load (default is first available face).
:param encoding: Which font encoding to use (default is Unicode). Possible :param encoding: Which font encoding to use (default is Unicode). Possible
encodings include (see the FreeType documentation for more encodings include (see the FreeType documentation for more

View File

@ -25,7 +25,12 @@ _viewers = []
def register(viewer, order=1): 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 viewer: The viewer to be registered.
:param order: :param order:

View File

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

View File

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

View File

@ -132,7 +132,7 @@ def _res_to_dpi(num, denom, exp):
calculated as (num / denom) * 10^exp and stored in dots per meter, calculated as (num / denom) * 10^exp and stored in dots per meter,
to floating-point dots per inch.""" to floating-point dots per inch."""
if denom != 0: if denom != 0:
return (254 * num * (10 ** exp)) / (10000 * denom) return (254 * num * (10**exp)) / (10000 * denom)
def _parse_jp2_header(fp): def _parse_jp2_header(fp):

View File

@ -46,6 +46,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
self._after_jpeg_open() self._after_jpeg_open()
def _after_jpeg_open(self, mpheader=None): def _after_jpeg_open(self, mpheader=None):
self._initial_size = self.size
self.mpinfo = mpheader if mpheader is not None else self._getmp() self.mpinfo = mpheader if mpheader is not None else self._getmp()
self.n_frames = self.mpinfo[0xB001] self.n_frames = self.mpinfo[0xB001]
self.__mpoffsets = [ self.__mpoffsets = [
@ -77,6 +78,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
segment = self.fp.read(2) segment = self.fp.read(2)
if not segment: if not segment:
raise ValueError("No data found for frame") raise ValueError("No data found for frame")
self._size = self._initial_size
if i16(segment) == 0xFFE1: # APP1 if i16(segment) == 0xFFE1: # APP1
n = i16(self.fp.read(2)) - 2 n = i16(self.fp.read(2)) - 2
self.info["exif"] = ImageFile._safe_read(self.fp, n) 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)) self.set_as_raw(img.getvalue(), ("1", 0, 1))
return 0, 0 return -1, 0
Image.register_decoder("MSP", MspDecoder) Image.register_decoder("MSP", MspDecoder)

View File

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

View File

@ -48,7 +48,7 @@ from ._binary import o32be as o32
logger = logging.getLogger(__name__) 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" _MAGIC = b"\211PNG\r\n\032\n"

View File

@ -49,26 +49,46 @@ class PpmImageFile(ImageFile.ImageFile):
format = "PPM" format = "PPM"
format_description = "Pbmplus image" format_description = "Pbmplus image"
def _token(self, s=b""): def _read_magic(self):
while True: # read until next whitespace magic = b""
# read until whitespace or longest available magic number
for _ in range(6):
c = self.fp.read(1) c = self.fp.read(1)
if not c or c in b_whitespace: if not c or c in b_whitespace:
break break
if c > b"\x79": magic += c
raise ValueError("Expected ASCII value, found binary") return magic
s = s + c
if len(s) > 9: def _read_token(self):
raise ValueError("Expected int, got > 9 digits") token = b""
return s 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): def _open(self):
magic_number = self._read_magic()
# check magic try:
s = self.fp.read(1) mode = MODES[magic_number]
if s != b"P": except KeyError:
raise SyntaxError("not a PPM file") raise SyntaxError("not a PPM file")
magic_number = self._token(s)
mode = MODES[magic_number]
self.custom_mimetype = { self.custom_mimetype = {
b"P4": "image/x-portable-bitmap", b"P4": "image/x-portable-bitmap",
@ -83,29 +103,19 @@ class PpmImageFile(ImageFile.ImageFile):
self.mode = rawmode = mode self.mode = rawmode = mode
for ix in range(3): for ix in range(3):
while True: token = int(self._read_token())
while True: if ix == 0: # token is the x size
s = self.fp.read(1) xsize = token
if s not in b_whitespace: elif ix == 1: # token is the y size
break ysize = token
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
if mode == "1": if mode == "1":
break break
elif ix == 2: elif ix == 2: # token is maxval
# maxgrey maxval = token
if s > 255: if maxval > 255:
if not mode == "L": if not mode == "L":
raise ValueError(f"Too many colors for band: {s}") raise ValueError(f"Too many colors for band: {token}")
if s < 2 ** 16: if maxval < 2**16:
self.mode = "I" self.mode = "I"
rawmode = "I;16B" rawmode = "I;16B"
else: else:
@ -126,7 +136,7 @@ def _save(im, fp, filename):
elif im.mode == "L": elif im.mode == "L":
rawmode, head = "L", b"P5" rawmode, head = "L", b"P5"
elif im.mode == "I": elif im.mode == "I":
if im.getextrema()[1] < 2 ** 16: if im.getextrema()[1] < 2**16:
rawmode, head = "I;16B", b"P5" rawmode, head = "I;16B", b"P5"
else: else:
rawmode, head = "I;32B", b"P5" rawmode, head = "I;32B", b"P5"

View File

@ -155,14 +155,6 @@ class PsdImageFile(ImageFile.ImageFile):
# return layer number (0=image, 1..max=layers) # return layer number (0=image, 1..max=layers)
return self.frame 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): def _close__fp(self):
try: try:
if self.__fp != self.fp: if self.__fp != self.fp:

View File

@ -493,7 +493,7 @@ class ImageFileDirectory_v2(MutableMapping):
endianness. endianness.
:param prefix: Override the endianness of the file. :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)") raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)")
self._prefix = prefix if prefix is not None else ifh[:2] self._prefix = prefix if prefix is not None else ifh[:2]
if self._prefix == MM: if self._prefix == MM:
@ -577,9 +577,9 @@ class ImageFileDirectory_v2(MutableMapping):
else TiffTags.SIGNED_RATIONAL else TiffTags.SIGNED_RATIONAL
) )
elif all(isinstance(v, int) for v in values): elif all(isinstance(v, int) for v in values):
if all(0 <= v < 2 ** 16 for v in values): if all(0 <= v < 2**16 for v in values):
self.tagtype[tag] = TiffTags.SHORT self.tagtype[tag] = TiffTags.SHORT
elif all(-(2 ** 15) < v < 2 ** 15 for v in values): elif all(-(2**15) < v < 2**15 for v in values):
self.tagtype[tag] = TiffTags.SIGNED_SHORT self.tagtype[tag] = TiffTags.SIGNED_SHORT
else: else:
self.tagtype[tag] = ( self.tagtype[tag] = (
@ -734,7 +734,7 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_writer(5) @_register_writer(5)
def write_rational(self, *values): def write_rational(self, *values):
return b"".join( return b"".join(
self._pack("2L", *_limit_rational(frac, 2 ** 32 - 1)) for frac in values self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values
) )
@_register_loader(7, 1) @_register_loader(7, 1)
@ -757,7 +757,7 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_writer(10) @_register_writer(10)
def write_signed_rational(self, *values): def write_signed_rational(self, *values):
return b"".join( return b"".join(
self._pack("2l", *_limit_signed_rational(frac, 2 ** 31 - 1, -(2 ** 31))) self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))
for frac in values for frac in values
) )
@ -1670,7 +1670,7 @@ def _save(im, fp, filename):
strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip
strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip
ifd[ROWSPERSTRIP] = rows_per_strip ifd[ROWSPERSTRIP] = rows_per_strip
if strip_byte_counts >= 2 ** 16: if strip_byte_counts >= 2**16:
ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
stride * im.size[1] - strip_byte_counts * (strips_per_image - 1), stride * im.size[1] - strip_byte_counts * (strips_per_image - 1),

View File

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

View File

@ -25,7 +25,7 @@ from . import Image, ImageFile
# XBM header # XBM header
xbm_head = re.compile( 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"#define[ \t]+.*_height[ \t]+(?P<height>[0-9]+)[\r\n]+"
b"(?P<hotspot>" b"(?P<hotspot>"
b"#define[ \t]+[^_]*_x_hot[ \t]+(?P<xhot>[0-9]+)[\r\n]+" b"#define[ \t]+[^_]*_x_hot[ \t]+(?P<xhot>[0-9]+)[\r\n]+"
@ -52,18 +52,19 @@ class XbmImageFile(ImageFile.ImageFile):
m = xbm_head.match(self.fp.read(512)) m = xbm_head.match(self.fp.read(512))
if m: if not m:
raise SyntaxError("not a XBM file")
xsize = int(m.group("width")) xsize = int(m.group("width"))
ysize = int(m.group("height")) ysize = int(m.group("height"))
if m.group("hotspot"): if m.group("hotspot"):
self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot")))
self.mode = "1" self.mode = "1"
self._size = xsize, ysize self._size = xsize, ysize
self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] self.tile = [("xbm", (0, 0) + self.size, m.end(), None)]
def _save(im, fp, filename): def _save(im, fp, filename):

View File

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

View File

@ -37,7 +37,7 @@
#define MAX(a, b) (a) > (b) ? (a) : (b) #define MAX(a, b) (a) > (b) ? (a) : (b)
#define MIN(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) */ /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */
#define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114) #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(); return (Imaging)ImagingError_ModeError();
} }
if (!((strcmp(imIn->mode, "RGB") == 0 || strcmp(imIn->mode, "1") == 0 || if (strcmp(imIn->mode, "RGB") == 0 && strcmp(mode, "RGBA") == 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) {
convert = rgb2rgba; 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) { if (strcmp(imIn->mode, "1") == 0) {
convert = bit2rgb; convert = bit2rgb;
} else if (strcmp(imIn->mode, "I") == 0) { } 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; convert = l2rgb;
} }
g = b = r; 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); 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); paste_mask_L(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize);
ImagingSectionLeave(&cookie); ImagingSectionLeave(&cookie);
} else if (strcmp(imMask->mode, "RGBA") == 0) { } else if (strcmp(imMask->mode, "LA") == 0 || strcmp(imMask->mode, "RGBA") == 0) {
ImagingSectionEnter(&cookie); ImagingSectionEnter(&cookie);
paste_mask_RGBA( paste_mask_RGBA(
imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize);

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