Merge branch 'master' into patch-3

This commit is contained in:
Meithal 2021-07-04 13:14:43 +02:00
commit d9c14e9ccd
220 changed files with 3839 additions and 1909 deletions

View File

@ -23,9 +23,9 @@ install:
- 7z x pillow-depends.zip -oc:\ - 7z x pillow-depends.zip -oc:\
- mv c:\pillow-depends-master c:\pillow-depends - mv c:\pillow-depends-master c:\pillow-depends
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images - xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
- 7z x ..\pillow-depends\nasm-2.14.02-win64.zip -oc:\ - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
- ..\pillow-depends\gs9533w32.exe /S - ..\pillow-depends\gs9540w32.exe /S
- path c:\nasm-2.14.02;C:\Program Files (x86)\gs\gs9.53.3\bin;%PATH% - path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.54.0\bin;%PATH%
- cd c:\pillow\winbuild\ - cd c:\pillow\winbuild\
- ps: | - ps: |
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\

View File

@ -24,6 +24,7 @@ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
PYTHONOPTIMIZE=0 python3 -m pip install cffi PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install olefile python3 -m pip install olefile
python3 -m pip install -U pytest python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov
@ -33,10 +34,6 @@ python3 -m pip install test-image-results
# TODO Remove condition when numpy supports 3.10 # TODO Remove condition when numpy supports 3.10
if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi
# TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+:
if [ "$GHA_PYTHON_VERSION" == "3.8" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi
if [ "$GHA_PYTHON_VERSION" == "3.9" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi
# PyQt5 doesn't support PyPy3 # PyQt5 doesn't support PyPy3
# Wheel doesn't yet support 3.10 # Wheel doesn't yet support 3.10
if [[ $GHA_PYTHON_VERSION == 3.* && $GHA_PYTHON_VERSION != "3.10-dev" ]]; then if [[ $GHA_PYTHON_VERSION == 3.* && $GHA_PYTHON_VERSION != "3.10-dev" ]]; then

View File

@ -4,4 +4,4 @@ set -e
python3 -c "from PIL import Image" python3 -c "from PIL import Image"
python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests $REVERSE

View File

@ -6,6 +6,7 @@ brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype op
PYTHONOPTIMIZE=0 python3 -m pip install cffi PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install olefile python3 -m pip install olefile
python3 -m pip install -U pytest python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov
@ -17,9 +18,5 @@ echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openbla
# TODO Remove condition when numpy supports 3.10 # TODO Remove condition when numpy supports 3.10
if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi
# TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+:
if [ "$GHA_PYTHON_VERSION" == "3.8" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi
if [ "$GHA_PYTHON_VERSION" == "3.9" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -21,8 +21,8 @@ jobs:
centos-7-amd64, centos-7-amd64,
centos-8-amd64, centos-8-amd64,
debian-10-buster-x86, debian-10-buster-x86,
fedora-32-amd64,
fedora-33-amd64, fedora-33-amd64,
fedora-34-amd64,
ubuntu-18.04-bionic-amd64, ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64, ubuntu-20.04-focal-amd64,
] ]

View File

@ -8,19 +8,13 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["pypy-3.6", "pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10-dev"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev"]
architecture: ["x86", "x64"] architecture: ["x86", "x64"]
include: include:
- architecture: "x86" # PyPy3.6 only ships 32-bit binaries for Windows
platform-vcvars: "x86"
platform-msbuild: "Win32"
- architecture: "x64"
platform-vcvars: "x86_amd64"
platform-msbuild: "x64"
exclude:
# PyPy does not support 64-bit on Windows
- python-version: "pypy-3.6" - python-version: "pypy-3.6"
architecture: "x64" architecture: "x86"
# PyPy 7.3.4+ only ships 64-bit binaries for Windows
- python-version: "pypy-3.7" - python-version: "pypy-3.7"
architecture: "x64" architecture: "x64"
timeout-minutes: 30 timeout-minutes: 30
@ -57,22 +51,17 @@ jobs:
- name: Print build system information - name: Print build system information
run: python .github/workflows/system-info.py run: python .github/workflows/system-info.py
- name: python -m pip install wheel pytest pytest-cov pytest-timeout - name: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
run: python -m pip install wheel pytest pytest-cov pytest-timeout run: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
# TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+:
- name: Upgrade setuptools
if: "contains(matrix.python-version, '3.8') || contains(matrix.python-version, '3.9')"
run: python -m pip install -U "setuptools>=49.3.2"
- name: Install dependencies - name: Install dependencies
id: install id: install
run: | run: |
7z x winbuild\depends\nasm-2.14.02-win64.zip "-o$env:RUNNER_WORKSPACE\" 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
echo "$env:RUNNER_WORKSPACE\nasm-2.14.02" >> $env:GITHUB_PATH echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
winbuild\depends\gs9533w32.exe /S winbuild\depends\gs9540w32.exe /S
echo "C:\Program Files (x86)\gs\gs9.53.3\bin" >> $env:GITHUB_PATH echo "C:\Program Files (x86)\gs\gs9.54.0\bin" >> $env:GITHUB_PATH
xcopy /S /Y winbuild\depends\test_images\* Tests\images\ xcopy /S /Y winbuild\depends\test_images\* Tests\images\

View File

@ -24,6 +24,7 @@ jobs:
include: include:
- python-version: "3.6" - python-version: "3.6"
PYTHONOPTIMIZE: 1 PYTHONOPTIMIZE: 1
REVERSE: "--reverse"
- python-version: "3.7" - python-version: "3.7"
PYTHONOPTIMIZE: 2 PYTHONOPTIMIZE: 2
# Include new variables for Codecov # Include new variables for Codecov
@ -80,6 +81,9 @@ jobs:
- name: Test - name: Test
run: | run: |
if [ $REVERSE ]; then
python3 -m pip install pytest-reverse
fi
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
else else
@ -87,6 +91,7 @@ jobs:
fi fi
env: env:
PYTHONOPTIMIZE: ${{ matrix.PYTHONOPTIMIZE }} PYTHONOPTIMIZE: ${{ matrix.PYTHONOPTIMIZE }}
REVERSE: ${{ matrix.REVERSE }}
- name: Prepare to upload errors - name: Prepare to upload errors
if: failure() if: failure()
@ -103,7 +108,7 @@ jobs:
- name: Docs - name: Docs
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.9 if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.9
run: | run: |
python3 -m pip install sphinx-issues sphinx-removed-in sphinx-rtd-theme python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph
make doccheck make doccheck
- name: After success - name: After success

View File

@ -2,9 +2,204 @@
Changelog (Pillow) Changelog (Pillow)
================== ==================
8.2.0 (unreleased) 8.3.0 (2021-07-01)
------------------ ------------------
- Use snprintf instead of sprintf. CVE-2021-34552 #5567
[radarhere]
- Limit TIFF strip size when saving with LibTIFF #5514
[kmilos]
- Allow ICNS save on all operating systems #4526
[baletu, radarhere, newpanjing, hugovk]
- De-zigzag JPEG's DQT when loading; deprecate convert_dict_qtables #4989
[gofr, radarhere]
- Replaced xml.etree.ElementTree #5565
[radarhere]
- Moved CVE image to pillow-depends #5561
[radarhere]
- Added tag data for IFD groups #5554
[radarhere]
- Improved ImagePalette #5552
[radarhere]
- Add DDS saving #5402
[radarhere]
- Improved getxmp() #5455
[radarhere]
- Convert to float for comparison with float in IFDRational __eq__ #5412
[radarhere]
- Allow getexif() to access TIFF tag_v2 data #5416
[radarhere]
- Read FITS image mode and size #5405
[radarhere]
- Merge parallel horizontal edges in ImagingDrawPolygon #5347
[radarhere, hrdrq]
- Use transparency behind first GIF frame and when disposing to background #5557
[radarhere, zewt]
- Avoid unstable nature of qsort in Quant.c #5367
[radarhere]
- Copy palette to new images in ImageOps expand #5551
[radarhere]
- Ensure palette string matches RGB mode #5549
[radarhere]
- Do not modify EXIF of original image instance in exif_transpose() #5547
[radarhere]
- Fixed default numresolution for small JPEG2000 images #5540
[radarhere]
- Added DDS BC5 reading #5501
[radarhere]
- Raise an error if ImageDraw.textbbox is used without a TrueType font #5510
[radarhere]
- Added ICO saving in BMP format #5513
[radarhere]
- Ensure PNG seeks to end of previous chunk at start of load_end #5493
[radarhere]
- Do not allow TIFF to seek to a past frame #5473
[radarhere]
- Avoid race condition when displaying images with eog #5507
[mconst]
- Added specific error messages when ink has incorrect number of bands #5504
[radarhere]
- Allow converting an image to a numpy array to raise errors #5379
[radarhere]
- Removed DPI rounding from BMP, JPEG, PNG and WMF loading #5476, #5470
[radarhere]
- Remove spikes when drawing thin pieslices #5460
[xtsm]
- Updated default value for SAMPLESPERPIXEL TIFF tag #5452
[radarhere]
- Removed TIFF DPI rounding #5446
[radarhere, hugovk]
- Include code in WebP error #5471
[radarhere]
- Do not alter pixels outside mask when drawing text on an image with transparency #5434
[radarhere]
- Reset handle when seeking backwards in TIFF #5443
[radarhere]
- Replace sys.stdout with sys.stdout.buffer when saving #5437
[radarhere]
- Fixed UNDEFINED TIFF tag of length 0 being changed in roundtrip #5426
[radarhere]
- Fixed bug when checking FreeType2 version if it is not installed #5445
[radarhere]
- Do not round dimensions when saving PDF #5459
[radarhere]
- Added ImageOps contain() #5417
[radarhere, hugovk]
- Changed WebP default "method" value to 4 #5450
[radarhere]
- Switched to saving 1-bit PDFs with DCTDecode #5430
[radarhere]
- Use bpp from ICO header #5429
[radarhere]
- Corrected JPEG APP14 transform value #5408
[radarhere]
- Changed TIFF tag 33723 length to 1 #5425
[radarhere]
- Changed ImageMorph incorrect mode errors to ValueError #5414
[radarhere]
- Add EXIF tags specified in EXIF 2.32 #5419
[gladiusglad]
- Treat previous contents of first GIF frame as transparent #5391
[radarhere]
- For special image modes, revert default resize resampling to NEAREST #5411
[radarhere]
- JPEG2000: Support decoding subsampled RGB and YCbCr images #4996
[nulano, radarhere]
- Stop decoding BC1 punchthrough alpha in BC2&3 #4144
[jansol]
- Use zero if GIF background color index is missing #5390
[radarhere]
- Fixed ensuring that GIF previous frame was loaded #5386
[radarhere]
- Valgrind fixes #5397
[wiredfool]
- Round down the radius in rounded_rectangle #5382
[radarhere]
- Fixed reading uncompressed RGB data from DDS #5383
[radarhere]
8.2.0 (2021-04-01)
------------------
- Added getxmp() method #5144
[UrielMaD, radarhere]
- Add ImageShow support for GraphicsMagick #5349
[latosha-maltba, radarhere]
- Do not load transparent pixels from subsequent GIF frames #5333
[zewt, radarhere]
- Use LZW encoding when saving GIF images #5291
[raygard]
- Set all transparent colors to be equal in quantize() #5282
[radarhere]
- Allow PixelAccess to use Python __int__ when parsing x and y #5206
[radarhere]
- Removed Image._MODEINFO #5316
[radarhere]
- Add preserve_tone option to autocontrast #5350
[elejke, radarhere]
- Fixed linear_gradient and radial_gradient I and F modes #5274 - Fixed linear_gradient and radial_gradient I and F modes #5274
[radarhere] [radarhere]

View File

@ -102,6 +102,13 @@ sdist:
test: test:
pytest -qq pytest -qq
.PHONY: valgrind
valgrind:
python3 -c "import pytest_valgrind" || pip3 install pytest-valgrind
PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
--log-file=/tmp/valgrind-output \
python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output
.PHONY: readme .PHONY: readme
readme: readme:
python3 setup.py --long-description | markdown2 > .long-description.html && open .long-description.html python3 setup.py --long-description | markdown2 > .long-description.html && open .long-description.html

View File

@ -39,9 +39,12 @@ As of 2019, Pillow development is
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img <a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
alt="AppVeyor CI build status (Windows)" alt="AppVeyor CI build status (Windows)"
src="https://img.shields.io/appveyor/build/python-pillow/Pillow/master.svg?label=Windows%20build"></a> src="https://img.shields.io/appveyor/build/python-pillow/Pillow/master.svg?label=Windows%20build"></a>
<a href="https://github.com/python-pillow/pillow-wheels/actions"><img
alt="GitHub Actions wheels build status (Wheels)"
src="https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg"></a>
<a href="https://travis-ci.com/github/python-pillow/pillow-wheels"><img <a href="https://travis-ci.com/github/python-pillow/pillow-wheels"><img
alt="Travis CI build status (macOS)" alt="Travis CI wheels build status (aarch64)"
src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/master.svg?label=macOS%20build"></a> src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/master.svg?label=aarch64%20wheels"></a>
<a href="https://codecov.io/gh/python-pillow/Pillow"><img <a href="https://codecov.io/gh/python-pillow/Pillow"><img
alt="Code coverage" alt="Code coverage"
src="https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg"></a> src="https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg"></a>

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
import sys import sys

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from PIL import Image from PIL import Image

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
import pytest import pytest
from PIL import Image from PIL import Image

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# Reproductions/tests for OOB read errors in FliDecode.c # Reproductions/tests for OOB read errors in FliDecode.c

View File

@ -1,7 +1,4 @@
import io import io
import warnings
import pytest
def pytest_report_header(config): def pytest_report_header(config):
@ -16,16 +13,19 @@ def pytest_report_header(config):
def pytest_configure(config): def pytest_configure(config):
config.addinivalue_line(
"markers",
"pil_noop_mark: A conditional mark where nothing special happens",
)
# We're marking some tests to ignore valgrind errors and XFAIL them. # We're marking some tests to ignore valgrind errors and XFAIL them.
# Ensure that the mark is defined # Ensure that the mark is defined
# even in cases where pytest-valgrind isn't installed # even in cases where pytest-valgrind isn't installed
try:
with warnings.catch_warnings(): config.addinivalue_line(
warnings.simplefilter("error") "markers",
try: "valgrind_known_error: Tests that have known issues with valgrind",
getattr(pytest.mark, "valgrind_known_error") )
except Exception: except Exception:
config.addinivalue_line( # valgrind is already installed
"markers", pass
"valgrind_known_error: Tests that have known issues with valgrind",
)

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
import base64 import base64
import os import os

View File

@ -173,6 +173,21 @@ def skip_unless_feature_version(feature, version_required, reason=None):
return pytest.mark.skipif(version_available < version_required, reason=reason) return pytest.mark.skipif(version_available < version_required, reason=reason)
def mark_if_feature_version(mark, feature, version_blacklist, reason=None):
if not features.check(feature):
return pytest.mark.pil_noop_mark()
if reason is None:
reason = f"{feature} is {version_blacklist}"
version_required = parse_version(version_blacklist)
version_available = parse_version(features.version(feature))
if (
version_available.major == version_required.major
and version_available.minor == version_required.minor
):
return mark(reason=reason)
return pytest.mark.pil_noop_mark()
@pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") @pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS")
class PillowLeakTestCase: class PillowLeakTestCase:
# requires unix/macOS # requires unix/macOS
@ -257,8 +272,23 @@ def netpbm_available():
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
def imagemagick_available(): def magick_command():
return bool(IMCONVERT and shutil.which(IMCONVERT)) if sys.platform == "win32":
magickhome = os.environ.get("MAGICK_HOME", "")
if magickhome:
imagemagick = [os.path.join(magickhome, "convert.exe")]
graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"]
else:
imagemagick = None
graphicsmagick = None
else:
imagemagick = ["convert"]
graphicsmagick = ["gm", "convert"]
if imagemagick and shutil.which(imagemagick[0]):
return imagemagick
elif graphicsmagick and shutil.which(graphicsmagick[0]):
return graphicsmagick
def on_appveyor(): def on_appveyor():
@ -296,14 +326,6 @@ def is_mingw():
return sysconfig.get_platform() == "mingw" return sysconfig.get_platform() == "mingw"
if sys.platform == "win32":
IMCONVERT = os.environ.get("MAGICK_HOME", "")
if IMCONVERT:
IMCONVERT = os.path.join(IMCONVERT, "convert.exe")
else:
IMCONVERT = "convert"
class cached_property: class cached_property:
def __init__(self, func): def __init__(self, func):
self.func = func self.func = func

Binary file not shown.

BIN
Tests/images/bc5_snorm.dds Normal file

Binary file not shown.

Binary file not shown.

BIN
Tests/images/bc5_unorm.dds Normal file

Binary file not shown.

BIN
Tests/images/bc5_unorm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
Tests/images/bc5s.dds Normal file

Binary file not shown.

BIN
Tests/images/bc5s.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 B

BIN
Tests/images/hopper.dds Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Tests/images/p_16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

BIN
Tests/images/p_16.tga Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

BIN
Tests/images/xmp_test.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@ -34,6 +34,7 @@ def main():
fuzzers.enable_decompressionbomb_error() fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
atheris.Fuzz() atheris.Fuzz()
fuzzers.disable_decompressionbomb_error()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -34,6 +34,7 @@ def main():
fuzzers.enable_decompressionbomb_error() fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
atheris.Fuzz() atheris.Fuzz()
fuzzers.disable_decompressionbomb_error()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -10,6 +10,11 @@ def enable_decompressionbomb_error():
warnings.simplefilter("error", Image.DecompressionBombWarning) warnings.simplefilter("error", Image.DecompressionBombWarning)
def disable_decompressionbomb_error():
ImageFile.LOAD_TRUNCATED_IMAGES = False
warnings.resetwarnings()
def fuzz_image(data): def fuzz_image(data):
# This will fail on some images in the corpus, as we have many # This will fail on some images in the corpus, as we have many
# invalid images in the test suite. # invalid images in the test suite.

View File

@ -0,0 +1,16 @@
{
<py3_8_encode_current_locale>
Memcheck:Cond
...
fun:encode_current_locale
}
{
<libtiff_zlib>
Memcheck:Cond
fun:inflate
fun:ZIPDecode
fun:_TIFFReadEncodedTileAndAllocBuffer
...
}

View File

@ -2,12 +2,19 @@ import subprocess
import sys import sys
import fuzzers import fuzzers
import packaging
import pytest import pytest
from PIL import Image from PIL import Image, features
if sys.platform.startswith("win32"): if sys.platform.startswith("win32"):
pytest.skip("Fuzzer is linux only", allow_module_level=True) pytest.skip("Fuzzer is linux only", allow_module_level=True)
if features.check("libjpeg_turbo"):
version = packaging.version.parse(features.version("libjpeg_turbo"))
if version.major == 2 and version.minor == 0:
pytestmark = pytest.mark.valgrind_known_error(
reason="Known failing with libjpeg_turbo 2.0"
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -37,6 +44,8 @@ def test_fuzz_images(path):
): ):
# Known Image.* exceptions # Known Image.* exceptions
assert True assert True
finally:
fuzzers.disable_decompressionbomb_error()
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -10,8 +10,7 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
class TestDecompressionBomb: class TestDecompressionBomb:
@classmethod def teardown_method(self, method):
def teardown_class(cls):
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
def test_no_warning_small_file(self): def test_no_warning_small_file(self):
@ -52,6 +51,7 @@ class TestDecompressionBomb:
with Image.open(TEST_FILE): with Image.open(TEST_FILE):
pass pass
@pytest.mark.xfail(reason="different exception")
def test_exception_ico(self): def test_exception_ico(self):
with pytest.raises(Image.DecompressionBombError): with pytest.raises(Image.DecompressionBombError):
with Image.open("Tests/images/decompression_bomb.ico"): with Image.open("Tests/images/decompression_bomb.ico"):

View File

@ -249,8 +249,8 @@ def test_apng_mode():
assert im.mode == "P" assert im.mode == "P"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im = im.convert("RGBA") im = im.convert("RGBA")
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (255, 0, 0, 0)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (255, 0, 0, 0)
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
assert im.mode == "P" assert im.mode == "P"
@ -312,7 +312,7 @@ def test_apng_syntax_errors():
exception = e exception = e
assert exception is None assert exception is None
with pytest.raises(SyntaxError): with pytest.raises(OSError):
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im.load() im.load()

View File

@ -1,3 +1,5 @@
import pytest
from PIL import Image from PIL import Image
from .helper import assert_image_equal_tofile from .helper import assert_image_equal_tofile
@ -16,3 +18,22 @@ def test_load_blp2_dxt1():
def test_load_blp2_dxt1a(): def test_load_blp2_dxt1a():
with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
@pytest.mark.parametrize(
"test_file",
[
"Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp",
"Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp",
"Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp",
"Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp",
"Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp",
"Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp",
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
],
)
def test_crashes(test_file):
with open(test_file, "rb") as f:
with Image.open(f) as im:
with pytest.raises(OSError):
im.load()

View File

@ -63,7 +63,7 @@ def test_dpi():
output.seek(0) output.seek(0)
with Image.open(output) as reloaded: with Image.open(output) as reloaded:
assert reloaded.info["dpi"] == dpi assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161)
def test_save_bmp_with_dpi(tmp_path): def test_save_bmp_with_dpi(tmp_path):
@ -71,6 +71,7 @@ def test_save_bmp_with_dpi(tmp_path):
# Arrange # Arrange
outfile = str(tmp_path / "temp.jpg") outfile = str(tmp_path / "temp.jpg")
with Image.open("Tests/images/hopper.bmp") as im: with Image.open("Tests/images/hopper.bmp") as im:
assert im.info["dpi"] == (95.98654816726399, 95.98654816726399)
# Act # Act
im.save(outfile, "JPEG", dpi=im.info["dpi"]) im.save(outfile, "JPEG", dpi=im.info["dpi"])
@ -78,31 +79,17 @@ def test_save_bmp_with_dpi(tmp_path):
# Assert # Assert
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
reloaded.load() reloaded.load()
assert im.info["dpi"] == reloaded.info["dpi"] assert reloaded.info["dpi"] == (96, 96)
assert im.size == reloaded.size assert reloaded.size == im.size
assert reloaded.format == "JPEG" assert reloaded.format == "JPEG"
def test_load_dpi_rounding(): def test_save_float_dpi(tmp_path):
# Round up
with Image.open("Tests/images/hopper.bmp") as im:
assert im.info["dpi"] == (96, 96)
# Round down
with Image.open("Tests/images/hopper_roundDown.bmp") as im:
assert im.info["dpi"] == (72, 72)
def test_save_dpi_rounding(tmp_path):
outfile = str(tmp_path / "temp.bmp") outfile = str(tmp_path / "temp.bmp")
with Image.open("Tests/images/hopper.bmp") as im: with Image.open("Tests/images/hopper.bmp") as im:
im.save(outfile, dpi=(72.2, 72.2)) im.save(outfile, dpi=(72.21216100543306, 72.21216100543306))
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
assert reloaded.info["dpi"] == (72, 72) assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306)
im.save(outfile, dpi=(72.8, 72.8))
with Image.open(outfile) as reloaded:
assert reloaded.info["dpi"] == (73, 73)
def test_load_dib(): def test_load_dib():

View File

@ -5,16 +5,21 @@ import pytest
from PIL import DdsImagePlugin, Image from PIL import DdsImagePlugin, Image
from .helper import assert_image_equal, assert_image_equal_tofile from .helper import assert_image_equal, assert_image_equal_tofile, hopper
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
TEST_FILE_BC5S = "Tests/images/bc5s.dds"
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds"
TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds"
TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds" TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds"
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/uncompressed_rgb.dds" TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
def test_sanity_dxt1(): def test_sanity_dxt1():
@ -31,6 +36,19 @@ def test_sanity_dxt1():
assert_image_equal(im, target) assert_image_equal(im, target)
def test_sanity_dxt3():
"""Check DXT3 images can be opened"""
with Image.open(TEST_FILE_DXT3) as im:
im.load()
assert im.format == "DDS"
assert im.mode == "RGBA"
assert im.size == (256, 256)
assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png"))
def test_sanity_dxt5(): def test_sanity_dxt5():
"""Check DXT5 images can be opened""" """Check DXT5 images can be opened"""
@ -44,17 +62,28 @@ def test_sanity_dxt5():
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png")) assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
def test_sanity_dxt3(): @pytest.mark.parametrize(
"""Check DXT3 images can be opened""" ("image_path", "expected_path"),
(
# hexeditted to be typeless
(TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM),
(TEST_FILE_DX10_BC5_UNORM, TEST_FILE_DX10_BC5_UNORM),
# hexeditted to use DX10 FourCC
(TEST_FILE_DX10_BC5_SNORM, TEST_FILE_BC5S),
(TEST_FILE_BC5S, TEST_FILE_BC5S),
),
)
def test_dx10_bc5(image_path, expected_path):
"""Check DX10 BC5 images can be opened"""
with Image.open(TEST_FILE_DXT3) as im: with Image.open(image_path) as im:
im.load() im.load()
assert im.format == "DDS" assert im.format == "DDS"
assert im.mode == "RGBA" assert im.mode == "RGB"
assert im.size == (256, 256) assert im.size == (256, 256)
assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png")) assert_image_equal_tofile(im, expected_path.replace(".dds", ".png"))
def test_dx10_bc7(): def test_dx10_bc7():
@ -124,37 +153,44 @@ def test_unimplemented_dxgi_format():
def test_uncompressed_rgb(): def test_uncompressed_rgb():
"""Check uncompressed RGB images can be opened""" """Check uncompressed RGB images can be opened"""
# convert -format dds -define dds:compression=none hopper.jpg hopper.dds
with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im:
im.load() assert im.format == "DDS"
assert im.mode == "RGB"
assert im.size == (128, 128)
assert_image_equal_tofile(im, "Tests/images/hopper.png")
# Test image with alpha
with Image.open(TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA) as im:
assert im.format == "DDS" assert im.format == "DDS"
assert im.mode == "RGBA" assert im.mode == "RGBA"
assert im.size == (800, 600) assert im.size == (800, 600)
assert_image_equal_tofile( assert_image_equal_tofile(
im, TEST_FILE_UNCOMPRESSED_RGB.replace(".dds", ".png") im, TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA.replace(".dds", ".png")
) )
def test__validate_true(): def test__accept_true():
"""Check valid prefix""" """Check valid prefix"""
# Arrange # Arrange
prefix = b"DDS etc" prefix = b"DDS etc"
# Act # Act
output = DdsImagePlugin._validate(prefix) output = DdsImagePlugin._accept(prefix)
# Assert # Assert
assert output assert output
def test__validate_false(): def test__accept_false():
"""Check invalid prefix""" """Check invalid prefix"""
# Arrange # Arrange
prefix = b"something invalid" prefix = b"something invalid"
# Act # Act
output = DdsImagePlugin._validate(prefix) output = DdsImagePlugin._accept(prefix)
# Assert # Assert
assert not output assert not output
@ -187,7 +223,46 @@ def test_short_file():
short_file() short_file()
def test_dxt5_colorblock_alpha_issue_4142():
""" Check that colorblocks are decoded correctly in DXT5"""
with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im:
px = im.getpixel((0, 0))
assert px[0] != 0
assert px[1] != 0
assert px[2] != 0
px = im.getpixel((1, 0))
assert px[0] != 0
assert px[1] != 0
assert px[2] != 0
def test_unimplemented_pixel_format(): def test_unimplemented_pixel_format():
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
with Image.open("Tests/images/unimplemented_pixel_format.dds"): with Image.open("Tests/images/unimplemented_pixel_format.dds"):
pass pass
def test_save_unsupported_mode(tmp_path):
out = str(tmp_path / "temp.dds")
im = hopper("HSV")
with pytest.raises(OSError):
im.save(out)
@pytest.mark.parametrize(
("mode", "test_file"),
[
("RGB", "Tests/images/hopper.png"),
("RGBA", "Tests/images/pil123rgba.png"),
],
)
def test_save(mode, test_file, tmp_path):
out = str(tmp_path / "temp.dds")
with Image.open(test_file) as im:
assert im.mode == mode
im.save(out)
with Image.open(out) as reloaded:
assert_image_equal(im, reloaded)

View File

@ -8,6 +8,7 @@ from .helper import (
assert_image_similar, assert_image_similar,
assert_image_similar_tofile, assert_image_similar_tofile,
hopper, hopper,
mark_if_feature_version,
skip_unless_feature, skip_unless_feature,
) )
@ -64,7 +65,9 @@ def test_invalid_file():
EpsImagePlugin.EpsImageFile(invalid_file) EpsImagePlugin.EpsImageFile(invalid_file)
@pytest.mark.valgrind_known_error(reason="Known Failing") @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_cmyk(): def test_cmyk():
with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image:
@ -264,3 +267,15 @@ def test_emptyline():
assert image.mode == "RGB" assert image.mode == "RGB"
assert image.size == (460, 352) assert image.size == (460, 352)
assert image.format == "EPS" assert image.format == "EPS"
@pytest.mark.timeout(timeout=5)
@pytest.mark.parametrize(
"test_file",
["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
)
def test_timeout(test_file):
with open(test_file, "rb") as f:
with pytest.raises(Image.UnidentifiedImageError):
with Image.open(f):
pass

View File

@ -1,3 +1,5 @@
from io import BytesIO
import pytest import pytest
from PIL import FitsStubImagePlugin, Image from PIL import FitsStubImagePlugin, Image
@ -11,10 +13,8 @@ def test_open():
# Assert # Assert
assert im.format == "FITS" assert im.format == "FITS"
assert im.size == (128, 128)
# Dummy data from the stub assert im.mode == "L"
assert im.mode == "F"
assert im.size == (1, 1)
def test_invalid_file(): def test_invalid_file():
@ -35,6 +35,21 @@ def test_load():
im.load() im.load()
def test_truncated_fits():
# No END to headers
image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE"
with pytest.raises(OSError):
FitsStubImagePlugin.FITSStubImageFile(BytesIO(image_data))
def test_naxis_zero():
# This test image has been manually hexedited
# to set the number of data axes to zero
with pytest.raises(ValueError):
with Image.open("Tests/images/hopper_naxis_zero.fits"):
pass
def test_save(): def test_save():
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:

View File

@ -123,3 +123,18 @@ def test_seek():
im.seek(50) im.seek(50)
assert_image_equal_tofile(im, "Tests/images/a_fli.png") assert_image_equal_tofile(im, "Tests/images/a_fli.png")
@pytest.mark.parametrize(
"test_file",
[
"Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli",
"Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli",
],
)
@pytest.mark.timeout(timeout=3)
def test_timeouts(test_file):
with open(test_file, "rb") as f:
with Image.open(f) as im:
with pytest.raises(OSError):
im.load()

View File

@ -298,6 +298,12 @@ def test_eoferror():
im.seek(n_frames - 1) im.seek(n_frames - 1)
def test_first_frame_transparency():
with Image.open("Tests/images/first_frame_transparency.gif") as im:
px = im.load()
assert px[0, 0] == im.info["transparency"]
def test_dispose_none(): def test_dispose_none():
with Image.open("Tests/images/dispose_none.gif") as img: with Image.open("Tests/images/dispose_none.gif") as img:
try: try:
@ -331,6 +337,16 @@ def test_dispose_background():
pass pass
def test_transparent_dispose():
expected_colors = [(2, 1, 2), (0, 1, 0), (2, 1, 2)]
with Image.open("Tests/images/transparent_dispose.gif") as img:
for frame in range(3):
img.seek(frame)
for x in range(3):
color = img.getpixel((x, 0))
assert color == expected_colors[frame][x]
def test_dispose_previous(): def test_dispose_previous():
with Image.open("Tests/images/dispose_prev.gif") as img: with Image.open("Tests/images/dispose_prev.gif") as img:
try: try:
@ -341,6 +357,25 @@ def test_dispose_previous():
pass pass
def test_dispose_previous_first_frame():
with Image.open("Tests/images/dispose_prev_first_frame.gif") as im:
im.seek(1)
assert_image_equal_tofile(
im, "Tests/images/dispose_prev_first_frame_seeked.gif"
)
def test_previous_frame_loaded():
with Image.open("Tests/images/dispose_none.gif") as img:
img.load()
img.seek(1)
img.load()
img.seek(2)
with Image.open("Tests/images/dispose_none.gif") as img_skipped:
img_skipped.seek(2)
assert_image_equal(img_skipped, img)
def test_save_dispose(tmp_path): def test_save_dispose(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
im_list = [ im_list = [
@ -373,14 +408,15 @@ def test_save_dispose(tmp_path):
def test_dispose2_palette(tmp_path): def test_dispose2_palette(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
# 4 backgrounds: White, Grey, Black, Red # Four colors: white, grey, black, red
circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
im_list = [] im_list = []
for circle in circles: for circle in circles:
# Red background
img = Image.new("RGB", (100, 100), (255, 0, 0)) img = Image.new("RGB", (100, 100), (255, 0, 0))
# Red circle in center of each frame # Circle in center of each frame
d = ImageDraw.Draw(img) d = ImageDraw.Draw(img)
d.ellipse([(40, 40), (60, 60)], fill=circle) d.ellipse([(40, 40), (60, 60)], fill=circle)
@ -468,12 +504,25 @@ def test_dispose2_background(tmp_path):
assert im.getpixel((0, 0)) == 0 assert im.getpixel((0, 0)) == 0
def test_iss634(): def test_transparency_in_second_frame():
with Image.open("Tests/images/different_transparency.gif") as im:
assert im.info["transparency"] == 0
# Seek to the second frame
im.seek(im.tell() + 1)
assert im.info["transparency"] == 0
assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.gif")
def test_no_transparency_in_second_frame():
with Image.open("Tests/images/iss634.gif") as img: with Image.open("Tests/images/iss634.gif") as img:
# Seek to the second frame # Seek to the second frame
img.seek(img.tell() + 1) img.seek(img.tell() + 1)
assert "transparency" not in img.info
# All transparent pixels should be replaced with the color from the first frame # All transparent pixels should be replaced with the color from the first frame
assert img.histogram()[img.info["transparency"]] == 0 assert img.histogram()[255] == 0
def test_duration(tmp_path): def test_duration(tmp_path):
@ -717,10 +766,10 @@ def test_rgb_transparency(tmp_path):
# Single frame # Single frame
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
im.info["transparency"] = (255, 0, 0) im.info["transparency"] = (255, 0, 0)
pytest.warns(UserWarning, im.save, out) im.save(out)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert "transparency" not in reloaded.info assert "transparency" in reloaded.info
# Multiple frames # Multiple frames
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
@ -840,3 +889,11 @@ def test_extents():
assert im.size == (100, 100) assert im.size == (100, 100)
im.seek(1) im.seek(1)
assert im.size == (150, 150) assert im.size == (150, 150)
def test_missing_background():
# The Global Color Table Flag isn't set, so there is no background color index,
# but the disposal method is "Restore to background color"
with Image.open("Tests/images/missing_background.gif") as im:
im.seek(1)
assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.gif")

View File

@ -1,5 +1,4 @@
import io import io
import sys
import pytest import pytest
@ -28,7 +27,6 @@ def test_sanity():
assert im.format == "ICNS" assert im.format == "ICNS"
@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
def test_save(tmp_path): def test_save(tmp_path):
temp_file = str(tmp_path / "temp.icns") temp_file = str(tmp_path / "temp.icns")
@ -41,7 +39,6 @@ def test_save(tmp_path):
assert reread.format == "ICNS" assert reread.format == "ICNS"
@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
def test_save_append_images(tmp_path): def test_save_append_images(tmp_path):
temp_file = str(tmp_path / "temp.icns") temp_file = str(tmp_path / "temp.icns")
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128))
@ -57,7 +54,6 @@ def test_save_append_images(tmp_path):
assert_image_equal(reread, provided_im) assert_image_equal(reread, provided_im)
@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
def test_save_fp(): def test_save_fp():
fp = io.BytesIO() fp = io.BytesIO()

View File

@ -18,6 +18,12 @@ def test_sanity():
assert im.get_format_mimetype() == "image/x-icon" assert im.get_format_mimetype() == "image/x-icon"
def test_black_and_white():
with Image.open("Tests/images/black_and_white.ico") as im:
assert im.mode == "RGBA"
assert im.size == (16, 16)
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:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
@ -50,6 +56,35 @@ def test_save_to_bytes():
assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS))
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
def test_save_to_bytes_bmp(mode):
output = io.BytesIO()
im = hopper(mode)
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])
# The default image
output.seek(0)
with Image.open(output) as reloaded:
assert reloaded.info["sizes"] == {(32, 32), (64, 64)}
assert "RGBA" == reloaded.mode
assert (64, 64) == reloaded.size
assert reloaded.format == "ICO"
im = hopper(mode).resize((64, 64), Image.LANCZOS).convert("RGBA")
assert_image_equal(reloaded, im)
# The other one
output.seek(0)
with Image.open(output) as reloaded:
reloaded.size = (32, 32)
assert "RGBA" == reloaded.mode
assert (32, 32) == reloaded.size
assert reloaded.format == "ICO"
im = hopper(mode).resize((32, 32), Image.LANCZOS).convert("RGBA")
assert_image_equal(reloaded, im)
def test_incorrect_size(): def test_incorrect_size():
with Image.open(TEST_ICO_FILE) as im: with Image.open(TEST_ICO_FILE) as im:
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -119,5 +154,4 @@ def test_draw_reloaded(tmp_path):
im.save(outfile) im.save(outfile)
with Image.open(outfile) as im: with Image.open(outfile) as im:
im.save("Tests/images/hopper_draw.ico")
assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico")

View File

@ -24,9 +24,15 @@ from .helper import (
djpeg_available, djpeg_available,
hopper, hopper,
is_win32, is_win32,
mark_if_feature_version,
skip_unless_feature, skip_unless_feature,
) )
try:
import defusedxml.ElementTree as ElementTree
except ImportError:
ElementTree = None
TEST_FILE = "Tests/images/hopper.jpg" TEST_FILE = "Tests/images/hopper.jpg"
@ -116,7 +122,9 @@ class TestFileJpeg:
assert test(100, 200) == (100, 200) assert test(100, 200) == (100, 200)
assert test(0) is None # square pixels assert test(0) is None # square pixels
@pytest.mark.valgrind_known_error(reason="Known Failing") @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
def test_icc(self, tmp_path): def test_icc(self, tmp_path):
# Test ICC support # Test ICC support
with Image.open("Tests/images/rgb.jpg") as im1: with Image.open("Tests/images/rgb.jpg") as im1:
@ -156,7 +164,9 @@ class TestFileJpeg:
test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte
test(ImageFile.MAXBLOCK * 4 + 3) # large block test(ImageFile.MAXBLOCK * 4 + 3) # large block
@pytest.mark.valgrind_known_error(reason="Known Failing") @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
def test_large_icc_meta(self, tmp_path): def test_large_icc_meta(self, tmp_path):
# https://github.com/python-pillow/Pillow/issues/148 # https://github.com/python-pillow/Pillow/issues/148
# Sometimes the meta data on the icc_profile block is bigger than # Sometimes the meta data on the icc_profile block is bigger than
@ -423,7 +433,9 @@ class TestFileJpeg:
with Image.open(filename): with Image.open(filename):
pass pass
@pytest.mark.valgrind_known_error(reason="Known Failing") @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
def test_truncated_jpeg_should_read_all_the_data(self): def test_truncated_jpeg_should_read_all_the_data(self):
filename = "Tests/images/truncated_jpeg.jpg" filename = "Tests/images/truncated_jpeg.jpg"
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
@ -442,7 +454,9 @@ class TestFileJpeg:
with pytest.raises(OSError): with pytest.raises(OSError):
im.load() im.load()
@pytest.mark.valgrind_known_error(reason="Known Failing") @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
def test_qtables(self, tmp_path): def test_qtables(self, tmp_path):
def _n_qtables_helper(n, test_file): def _n_qtables_helper(n, test_file):
with Image.open(test_file) as im: with Image.open(test_file) as im:
@ -452,7 +466,7 @@ class TestFileJpeg:
assert len(im.quantization) == n assert len(im.quantization) == n
reloaded = self.roundtrip(im, qtables="keep") reloaded = self.roundtrip(im, qtables="keep")
assert im.quantization == reloaded.quantization assert im.quantization == reloaded.quantization
assert reloaded.quantization[0].typecode == "B" assert max(reloaded.quantization[0]) <= 255
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
qtables = im.quantization qtables = im.quantization
@ -464,7 +478,8 @@ class TestFileJpeg:
# valid bounds for baseline qtable # valid bounds for baseline qtable
bounds_qtable = [int(s) for s in ("255 1 " * 32).split(None)] bounds_qtable = [int(s) for s in ("255 1 " * 32).split(None)]
self.roundtrip(im, qtables=[bounds_qtable]) im2 = self.roundtrip(im, qtables=[bounds_qtable])
assert im2.quantization == {0: bounds_qtable}
# values from wizard.txt in jpeg9-a src package. # values from wizard.txt in jpeg9-a src package.
standard_l_qtable = [ standard_l_qtable = [
@ -575,6 +590,12 @@ class TestFileJpeg:
assert max(im2.quantization[0]) <= 255 assert max(im2.quantization[0]) <= 255
assert max(im2.quantization[1]) <= 255 assert max(im2.quantization[1]) <= 255
def test_convert_dict_qtables_deprecation(self):
with pytest.warns(DeprecationWarning):
qtable = {0: [1, 2, 3, 4]}
qtable2 = JpegImagePlugin.convert_dict_qtables(qtable)
assert qtable == qtable2
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
def test_load_djpeg(self): def test_load_djpeg(self):
with Image.open(TEST_FILE) as img: with Image.open(TEST_FILE) as img:
@ -647,15 +668,6 @@ class TestFileJpeg:
reloaded.load() reloaded.load()
assert im.info["dpi"] == reloaded.info["dpi"] assert im.info["dpi"] == reloaded.info["dpi"]
def test_load_dpi_rounding(self):
# Round up
with Image.open("Tests/images/iptc_roundUp.jpg") as im:
assert im.info["dpi"] == (44, 44)
# Round down
with Image.open("Tests/images/iptc_roundDown.jpg") as im:
assert im.info["dpi"] == (2, 2)
def test_save_dpi_rounding(self, tmp_path): def test_save_dpi_rounding(self, tmp_path):
outfile = str(tmp_path / "temp.jpg") outfile = str(tmp_path / "temp.jpg")
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
@ -726,7 +738,9 @@ class TestFileJpeg:
# OSError for unidentified image. # OSError for unidentified image.
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
@pytest.mark.valgrind_known_error(reason="Known Failing") @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
def test_exif_x_resolution(self, tmp_path): def test_exif_x_resolution(self, tmp_path):
with Image.open("Tests/images/flower.jpg") as im: with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif() exif = im.getexif()
@ -757,7 +771,9 @@ class TestFileJpeg:
# Act / Assert # Act / Assert
assert im._getexif()[306] == "2017:03:13 23:03:09" assert im._getexif()[306] == "2017:03:13 23:03:09"
@pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
def test_photoshop(self): def test_photoshop(self):
with Image.open("Tests/images/photoshop-200dpi.jpg") as im: with Image.open("Tests/images/photoshop-200dpi.jpg") as im:
assert im.info["photoshop"][0x03ED] == { assert im.info["photoshop"][0x03ED] == {
@ -782,6 +798,20 @@ class TestFileJpeg:
apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"]
assert [65504, 24] == apps_13_lengths assert [65504, 24] == apps_13_lengths
def test_adobe_transform(self):
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert im.info["adobe_transform"] == 1
with Image.open("Tests/images/pil_sample_cmyk.jpg") as im:
assert im.info["adobe_transform"] == 2
# This image has been manually hexedited
# so that the APP14 reports its length to be 11,
# leaving no room for "adobe_transform"
with Image.open("Tests/images/truncated_app14.jpg") as im:
assert "adobe" in im.info
assert "adobe_transform" not in im.info
def test_icc_after_SOF(self): def test_icc_after_SOF(self):
with Image.open("Tests/images/icc-after-SOF.jpg") as im: with Image.open("Tests/images/icc-after-SOF.jpg") as im:
assert im.info["icc_profile"] == b"profile" assert im.info["icc_profile"] == b"profile"
@ -805,6 +835,32 @@ class TestFileJpeg:
# Assert the entire file has not been read # Assert the entire file has not been read
assert 0 < buffer.max_pos < size assert 0 < buffer.max_pos < size
def test_getxmp(self):
with Image.open("Tests/images/xmp_test.jpg") as im:
if ElementTree is None:
with pytest.warns(UserWarning):
assert im.getxmp() == {}
else:
xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]
assert description["DerivedFrom"] == {
"documentID": "8367D410E636EA95B7DE7EBA1C43A412",
"originalDocumentID": "8367D410E636EA95B7DE7EBA1C43A412",
}
assert description["Look"]["Description"]["Group"]["Alt"]["li"] == {
"lang": "x-default",
"text": "Profiles",
}
assert description["ToneCurve"]["Seq"]["li"] == ["0, 0", "255, 255"]
# Attribute
assert description["Version"] == "10.4"
if ElementTree is not None:
with Image.open("Tests/images/hopper.jpg") as im:
assert im.getxmp() == {}
@pytest.mark.skipif(not is_win32(), reason="Windows only") @pytest.mark.skipif(not is_win32(), reason="Windows only")
@skip_unless_feature("jpg") @skip_unless_feature("jpg")

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