Merge branch 'master' into patch-3
|
@ -23,9 +23,9 @@ install:
|
|||
- 7z x pillow-depends.zip -oc:\
|
||||
- mv c:\pillow-depends-master c:\pillow-depends
|
||||
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
|
||||
- 7z x ..\pillow-depends\nasm-2.14.02-win64.zip -oc:\
|
||||
- ..\pillow-depends\gs9533w32.exe /S
|
||||
- path c:\nasm-2.14.02;C:\Program Files (x86)\gs\gs9.53.3\bin;%PATH%
|
||||
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
|
||||
- ..\pillow-depends\gs9540w32.exe /S
|
||||
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.54.0\bin;%PATH%
|
||||
- cd c:\pillow\winbuild\
|
||||
- ps: |
|
||||
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
||||
|
|
|
@ -24,6 +24,7 @@ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
|||
python3 -m pip install --upgrade pip
|
||||
PYTHONOPTIMIZE=0 python3 -m pip install cffi
|
||||
python3 -m pip install coverage
|
||||
python3 -m pip install defusedxml
|
||||
python3 -m pip install olefile
|
||||
python3 -m pip install -U pytest
|
||||
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
|
||||
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
|
||||
# Wheel doesn't yet support 3.10
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* && $GHA_PYTHON_VERSION != "3.10-dev" ]]; then
|
||||
|
|
|
@ -4,4 +4,4 @@ set -e
|
|||
|
||||
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
|
||||
|
|
5
.github/workflows/macos-install.sh
vendored
|
@ -6,6 +6,7 @@ brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype op
|
|||
|
||||
PYTHONOPTIMIZE=0 python3 -m pip install cffi
|
||||
python3 -m pip install coverage
|
||||
python3 -m pip install defusedxml
|
||||
python3 -m pip install olefile
|
||||
python3 -m pip install -U pytest
|
||||
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
|
||||
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
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
|
2
.github/workflows/test-docker.yml
vendored
|
@ -21,8 +21,8 @@ jobs:
|
|||
centos-7-amd64,
|
||||
centos-8-amd64,
|
||||
debian-10-buster-x86,
|
||||
fedora-32-amd64,
|
||||
fedora-33-amd64,
|
||||
fedora-34-amd64,
|
||||
ubuntu-18.04-bionic-amd64,
|
||||
ubuntu-20.04-focal-amd64,
|
||||
]
|
||||
|
|
31
.github/workflows/test-windows.yml
vendored
|
@ -8,19 +8,13 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
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"]
|
||||
include:
|
||||
- architecture: "x86"
|
||||
platform-vcvars: "x86"
|
||||
platform-msbuild: "Win32"
|
||||
- architecture: "x64"
|
||||
platform-vcvars: "x86_amd64"
|
||||
platform-msbuild: "x64"
|
||||
exclude:
|
||||
# PyPy does not support 64-bit on Windows
|
||||
# PyPy3.6 only ships 32-bit binaries for Windows
|
||||
- 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"
|
||||
architecture: "x64"
|
||||
timeout-minutes: 30
|
||||
|
@ -57,22 +51,17 @@ jobs:
|
|||
- name: Print build system information
|
||||
run: python .github/workflows/system-info.py
|
||||
|
||||
- name: python -m pip install wheel pytest pytest-cov pytest-timeout
|
||||
run: python -m pip install wheel pytest pytest-cov pytest-timeout
|
||||
|
||||
# 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: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
run: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
|
||||
- name: Install dependencies
|
||||
id: install
|
||||
run: |
|
||||
7z x winbuild\depends\nasm-2.14.02-win64.zip "-o$env:RUNNER_WORKSPACE\"
|
||||
echo "$env:RUNNER_WORKSPACE\nasm-2.14.02" >> $env:GITHUB_PATH
|
||||
7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
|
||||
echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
|
||||
|
||||
winbuild\depends\gs9533w32.exe /S
|
||||
echo "C:\Program Files (x86)\gs\gs9.53.3\bin" >> $env:GITHUB_PATH
|
||||
winbuild\depends\gs9540w32.exe /S
|
||||
echo "C:\Program Files (x86)\gs\gs9.54.0\bin" >> $env:GITHUB_PATH
|
||||
|
||||
xcopy /S /Y winbuild\depends\test_images\* Tests\images\
|
||||
|
||||
|
|
7
.github/workflows/test.yml
vendored
|
@ -24,6 +24,7 @@ jobs:
|
|||
include:
|
||||
- python-version: "3.6"
|
||||
PYTHONOPTIMIZE: 1
|
||||
REVERSE: "--reverse"
|
||||
- python-version: "3.7"
|
||||
PYTHONOPTIMIZE: 2
|
||||
# Include new variables for Codecov
|
||||
|
@ -80,6 +81,9 @@ jobs:
|
|||
|
||||
- name: Test
|
||||
run: |
|
||||
if [ $REVERSE ]; then
|
||||
python3 -m pip install pytest-reverse
|
||||
fi
|
||||
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
|
||||
xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
|
||||
else
|
||||
|
@ -87,6 +91,7 @@ jobs:
|
|||
fi
|
||||
env:
|
||||
PYTHONOPTIMIZE: ${{ matrix.PYTHONOPTIMIZE }}
|
||||
REVERSE: ${{ matrix.REVERSE }}
|
||||
|
||||
- name: Prepare to upload errors
|
||||
if: failure()
|
||||
|
@ -103,7 +108,7 @@ jobs:
|
|||
- name: Docs
|
||||
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.9
|
||||
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
|
||||
|
||||
- name: After success
|
||||
|
|
197
CHANGES.rst
|
@ -2,9 +2,204 @@
|
|||
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
|
||||
[radarhere]
|
||||
|
||||
|
|
7
Makefile
|
@ -102,6 +102,13 @@ sdist:
|
|||
test:
|
||||
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
|
||||
readme:
|
||||
python3 setup.py --long-description | markdown2 > .long-description.html && open .long-description.html
|
||||
|
|
|
@ -39,9 +39,12 @@ As of 2019, Pillow development is
|
|||
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
|
||||
alt="AppVeyor CI build status (Windows)"
|
||||
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
|
||||
alt="Travis CI build status (macOS)"
|
||||
src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/master.svg?label=macOS%20build"></a>
|
||||
alt="Travis CI wheels build status (aarch64)"
|
||||
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
|
||||
alt="Code coverage"
|
||||
src="https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg"></a>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Reproductions/tests for OOB read errors in FliDecode.c
|
||||
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import io
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_report_header(config):
|
||||
|
@ -16,16 +13,19 @@ def pytest_report_header(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.
|
||||
# Ensure that the mark is defined
|
||||
# even in cases where pytest-valgrind isn't installed
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
try:
|
||||
getattr(pytest.mark, "valgrind_known_error")
|
||||
except Exception:
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
"valgrind_known_error: Tests that have known issues with valgrind",
|
||||
)
|
||||
try:
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
"valgrind_known_error: Tests that have known issues with valgrind",
|
||||
)
|
||||
except Exception:
|
||||
# valgrind is already installed
|
||||
pass
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import os
|
||||
|
||||
|
|
BIN
Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf
Normal 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)
|
||||
|
||||
|
||||
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")
|
||||
class PillowLeakTestCase:
|
||||
# requires unix/macOS
|
||||
|
@ -257,8 +272,23 @@ def netpbm_available():
|
|||
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
|
||||
|
||||
|
||||
def imagemagick_available():
|
||||
return bool(IMCONVERT and shutil.which(IMCONVERT))
|
||||
def magick_command():
|
||||
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():
|
||||
|
@ -296,14 +326,6 @@ def is_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:
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
|
BIN
Tests/images/200x32_p_bl_raw_origin.tga
Normal file
BIN
Tests/images/bc5_snorm.dds
Normal file
BIN
Tests/images/bc5_typeless.dds
Normal file
BIN
Tests/images/bc5_unorm.dds
Normal file
BIN
Tests/images/bc5_unorm.png
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
Tests/images/bc5s.dds
Normal file
BIN
Tests/images/bc5s.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
Tests/images/black_and_white.ico
Normal file
After Width: | Height: | Size: 198 B |
BIN
Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif
Normal file
BIN
Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k
Normal file
BIN
Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif
Normal file
BIN
Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k
Normal file
BIN
Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k
Normal file
BIN
Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k
Normal file
BIN
Tests/images/different_transparency.gif
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
Tests/images/different_transparency_merged.gif
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
Tests/images/dispose_prev_first_frame.gif
Normal file
After Width: | Height: | Size: 530 B |
BIN
Tests/images/dispose_prev_first_frame_seeked.gif
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
Tests/images/dxt5-colorblock-alpha-issue-4142.dds
Normal file
BIN
Tests/images/first_frame_transparency.gif
Normal file
After Width: | Height: | Size: 972 B |
BIN
Tests/images/hopper.dds
Normal file
BIN
Tests/images/hopper_naxis_zero.fits
Normal file
BIN
Tests/images/hopper_resized.gif
Normal file
After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 48 KiB |
BIN
Tests/images/imagedraw/continuous_horizontal_edges_polygon.png
Normal file
After Width: | Height: | Size: 108 B |
After Width: | Height: | Size: 953 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 20 KiB |
BIN
Tests/images/missing_background.gif
Normal file
After Width: | Height: | Size: 660 B |
BIN
Tests/images/missing_background_first_frame.gif
Normal file
After Width: | Height: | Size: 950 B |
BIN
Tests/images/multipage_multiple_frame_loop.tiff
Normal file
BIN
Tests/images/multipage_out_of_order.tiff
Normal file
BIN
Tests/images/multipage_single_frame_loop.tiff
Normal file
BIN
Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif
Normal file
BIN
Tests/images/p_16.png
Normal file
After Width: | Height: | Size: 378 B |
BIN
Tests/images/p_16.tga
Normal file
BIN
Tests/images/padded_idat.png
Normal file
After Width: | Height: | Size: 104 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
Tests/images/transparent_background_text_L.png
Normal file
After Width: | Height: | Size: 350 B |
BIN
Tests/images/transparent_dispose.gif
Normal file
After Width: | Height: | Size: 95 B |
BIN
Tests/images/truncated_app14.jpg
Normal file
After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 143 KiB |
BIN
Tests/images/xmp_test.jpg
Normal file
After Width: | Height: | Size: 1.7 MiB |
|
@ -34,6 +34,7 @@ def main():
|
|||
fuzzers.enable_decompressionbomb_error()
|
||||
atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
|
||||
atheris.Fuzz()
|
||||
fuzzers.disable_decompressionbomb_error()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -34,6 +34,7 @@ def main():
|
|||
fuzzers.enable_decompressionbomb_error()
|
||||
atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
|
||||
atheris.Fuzz()
|
||||
fuzzers.disable_decompressionbomb_error()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -10,6 +10,11 @@ def enable_decompressionbomb_error():
|
|||
warnings.simplefilter("error", Image.DecompressionBombWarning)
|
||||
|
||||
|
||||
def disable_decompressionbomb_error():
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
warnings.resetwarnings()
|
||||
|
||||
|
||||
def fuzz_image(data):
|
||||
# This will fail on some images in the corpus, as we have many
|
||||
# invalid images in the test suite.
|
||||
|
|
16
Tests/oss-fuzz/python.supp
Normal 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
|
||||
...
|
||||
}
|
|
@ -2,12 +2,19 @@ import subprocess
|
|||
import sys
|
||||
|
||||
import fuzzers
|
||||
import packaging
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, features
|
||||
|
||||
if sys.platform.startswith("win32"):
|
||||
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(
|
||||
|
@ -37,6 +44,8 @@ def test_fuzz_images(path):
|
|||
):
|
||||
# Known Image.* exceptions
|
||||
assert True
|
||||
finally:
|
||||
fuzzers.disable_decompressionbomb_error()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
@ -10,8 +10,7 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
|
|||
|
||||
|
||||
class TestDecompressionBomb:
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
def teardown_method(self, method):
|
||||
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
|
||||
|
||||
def test_no_warning_small_file(self):
|
||||
|
@ -52,6 +51,7 @@ class TestDecompressionBomb:
|
|||
with Image.open(TEST_FILE):
|
||||
pass
|
||||
|
||||
@pytest.mark.xfail(reason="different exception")
|
||||
def test_exception_ico(self):
|
||||
with pytest.raises(Image.DecompressionBombError):
|
||||
with Image.open("Tests/images/decompression_bomb.ico"):
|
||||
|
|
|
@ -249,8 +249,8 @@ def test_apng_mode():
|
|||
assert im.mode == "P"
|
||||
im.seek(im.n_frames - 1)
|
||||
im = im.convert("RGBA")
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((0, 0)) == (255, 0, 0, 0)
|
||||
assert im.getpixel((64, 32)) == (255, 0, 0, 0)
|
||||
|
||||
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
|
||||
assert im.mode == "P"
|
||||
|
@ -312,7 +312,7 @@ def test_apng_syntax_errors():
|
|||
exception = e
|
||||
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:
|
||||
im.seek(im.n_frames - 1)
|
||||
im.load()
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import assert_image_equal_tofile
|
||||
|
@ -16,3 +18,22 @@ def test_load_blp2_dxt1():
|
|||
def test_load_blp2_dxt1a():
|
||||
with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im:
|
||||
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()
|
||||
|
|
|
@ -63,7 +63,7 @@ def test_dpi():
|
|||
|
||||
output.seek(0)
|
||||
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):
|
||||
|
@ -71,6 +71,7 @@ def test_save_bmp_with_dpi(tmp_path):
|
|||
# Arrange
|
||||
outfile = str(tmp_path / "temp.jpg")
|
||||
with Image.open("Tests/images/hopper.bmp") as im:
|
||||
assert im.info["dpi"] == (95.98654816726399, 95.98654816726399)
|
||||
|
||||
# Act
|
||||
im.save(outfile, "JPEG", dpi=im.info["dpi"])
|
||||
|
@ -78,31 +79,17 @@ def test_save_bmp_with_dpi(tmp_path):
|
|||
# Assert
|
||||
with Image.open(outfile) as reloaded:
|
||||
reloaded.load()
|
||||
assert im.info["dpi"] == reloaded.info["dpi"]
|
||||
assert im.size == reloaded.size
|
||||
assert reloaded.info["dpi"] == (96, 96)
|
||||
assert reloaded.size == im.size
|
||||
assert reloaded.format == "JPEG"
|
||||
|
||||
|
||||
def test_load_dpi_rounding():
|
||||
# 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):
|
||||
def test_save_float_dpi(tmp_path):
|
||||
outfile = str(tmp_path / "temp.bmp")
|
||||
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:
|
||||
assert reloaded.info["dpi"] == (72, 72)
|
||||
|
||||
im.save(outfile, dpi=(72.8, 72.8))
|
||||
with Image.open(outfile) as reloaded:
|
||||
assert reloaded.info["dpi"] == (73, 73)
|
||||
assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306)
|
||||
|
||||
|
||||
def test_load_dib():
|
||||
|
|
|
@ -5,16 +5,21 @@ import pytest
|
|||
|
||||
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_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_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_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_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():
|
||||
|
@ -31,6 +36,19 @@ def test_sanity_dxt1():
|
|||
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():
|
||||
"""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"))
|
||||
|
||||
|
||||
def test_sanity_dxt3():
|
||||
"""Check DXT3 images can be opened"""
|
||||
@pytest.mark.parametrize(
|
||||
("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()
|
||||
|
||||
assert im.format == "DDS"
|
||||
assert im.mode == "RGBA"
|
||||
assert im.mode == "RGB"
|
||||
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():
|
||||
|
@ -124,37 +153,44 @@ def test_unimplemented_dxgi_format():
|
|||
def test_uncompressed_rgb():
|
||||
"""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:
|
||||
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.mode == "RGBA"
|
||||
assert im.size == (800, 600)
|
||||
|
||||
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"""
|
||||
# Arrange
|
||||
prefix = b"DDS etc"
|
||||
|
||||
# Act
|
||||
output = DdsImagePlugin._validate(prefix)
|
||||
output = DdsImagePlugin._accept(prefix)
|
||||
|
||||
# Assert
|
||||
assert output
|
||||
|
||||
|
||||
def test__validate_false():
|
||||
def test__accept_false():
|
||||
"""Check invalid prefix"""
|
||||
# Arrange
|
||||
prefix = b"something invalid"
|
||||
|
||||
# Act
|
||||
output = DdsImagePlugin._validate(prefix)
|
||||
output = DdsImagePlugin._accept(prefix)
|
||||
|
||||
# Assert
|
||||
assert not output
|
||||
|
@ -187,7 +223,46 @@ def test_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():
|
||||
with pytest.raises(NotImplementedError):
|
||||
with Image.open("Tests/images/unimplemented_pixel_format.dds"):
|
||||
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)
|
||||
|
|
|
@ -8,6 +8,7 @@ from .helper import (
|
|||
assert_image_similar,
|
||||
assert_image_similar_tofile,
|
||||
hopper,
|
||||
mark_if_feature_version,
|
||||
skip_unless_feature,
|
||||
)
|
||||
|
||||
|
@ -64,7 +65,9 @@ def test_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")
|
||||
def test_cmyk():
|
||||
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.size == (460, 352)
|
||||
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
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import FitsStubImagePlugin, Image
|
||||
|
@ -11,10 +13,8 @@ def test_open():
|
|||
|
||||
# Assert
|
||||
assert im.format == "FITS"
|
||||
|
||||
# Dummy data from the stub
|
||||
assert im.mode == "F"
|
||||
assert im.size == (1, 1)
|
||||
assert im.size == (128, 128)
|
||||
assert im.mode == "L"
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
|
@ -35,6 +35,21 @@ def test_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():
|
||||
# Arrange
|
||||
with Image.open(TEST_FILE) as im:
|
||||
|
|
|
@ -123,3 +123,18 @@ def test_seek():
|
|||
im.seek(50)
|
||||
|
||||
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()
|
||||
|
|
|
@ -298,6 +298,12 @@ def test_eoferror():
|
|||
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():
|
||||
with Image.open("Tests/images/dispose_none.gif") as img:
|
||||
try:
|
||||
|
@ -331,6 +337,16 @@ def test_dispose_background():
|
|||
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():
|
||||
with Image.open("Tests/images/dispose_prev.gif") as img:
|
||||
try:
|
||||
|
@ -341,6 +357,25 @@ def test_dispose_previous():
|
|||
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):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
im_list = [
|
||||
|
@ -373,14 +408,15 @@ def test_save_dispose(tmp_path):
|
|||
def test_dispose2_palette(tmp_path):
|
||||
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)]
|
||||
|
||||
im_list = []
|
||||
for circle in circles:
|
||||
# Red background
|
||||
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.ellipse([(40, 40), (60, 60)], fill=circle)
|
||||
|
||||
|
@ -468,12 +504,25 @@ def test_dispose2_background(tmp_path):
|
|||
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:
|
||||
# Seek to the second frame
|
||||
img.seek(img.tell() + 1)
|
||||
assert "transparency" not in img.info
|
||||
|
||||
# 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):
|
||||
|
@ -717,10 +766,10 @@ def test_rgb_transparency(tmp_path):
|
|||
# Single frame
|
||||
im = Image.new("RGB", (1, 1))
|
||||
im.info["transparency"] = (255, 0, 0)
|
||||
pytest.warns(UserWarning, im.save, out)
|
||||
im.save(out)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert "transparency" not in reloaded.info
|
||||
assert "transparency" in reloaded.info
|
||||
|
||||
# Multiple frames
|
||||
im = Image.new("RGB", (1, 1))
|
||||
|
@ -840,3 +889,11 @@ def test_extents():
|
|||
assert im.size == (100, 100)
|
||||
im.seek(1)
|
||||
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")
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import io
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -28,7 +27,6 @@ def test_sanity():
|
|||
assert im.format == "ICNS"
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
|
||||
def test_save(tmp_path):
|
||||
temp_file = str(tmp_path / "temp.icns")
|
||||
|
||||
|
@ -41,7 +39,6 @@ def test_save(tmp_path):
|
|||
assert reread.format == "ICNS"
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
|
||||
def test_save_append_images(tmp_path):
|
||||
temp_file = str(tmp_path / "temp.icns")
|
||||
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)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
|
||||
def test_save_fp():
|
||||
fp = io.BytesIO()
|
||||
|
||||
|
|
|
@ -18,6 +18,12 @@ def test_sanity():
|
|||
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():
|
||||
with open("Tests/images/flower.jpg", "rb") as fp:
|
||||
with pytest.raises(SyntaxError):
|
||||
|
@ -50,6 +56,35 @@ def test_save_to_bytes():
|
|||
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():
|
||||
with Image.open(TEST_ICO_FILE) as im:
|
||||
with pytest.raises(ValueError):
|
||||
|
@ -119,5 +154,4 @@ def test_draw_reloaded(tmp_path):
|
|||
im.save(outfile)
|
||||
|
||||
with Image.open(outfile) as im:
|
||||
im.save("Tests/images/hopper_draw.ico")
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico")
|
||||
|
|
|
@ -24,9 +24,15 @@ from .helper import (
|
|||
djpeg_available,
|
||||
hopper,
|
||||
is_win32,
|
||||
mark_if_feature_version,
|
||||
skip_unless_feature,
|
||||
)
|
||||
|
||||
try:
|
||||
import defusedxml.ElementTree as ElementTree
|
||||
except ImportError:
|
||||
ElementTree = None
|
||||
|
||||
TEST_FILE = "Tests/images/hopper.jpg"
|
||||
|
||||
|
||||
|
@ -116,7 +122,9 @@ class TestFileJpeg:
|
|||
assert test(100, 200) == (100, 200)
|
||||
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):
|
||||
# Test ICC support
|
||||
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 * 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):
|
||||
# https://github.com/python-pillow/Pillow/issues/148
|
||||
# Sometimes the meta data on the icc_profile block is bigger than
|
||||
|
@ -423,7 +433,9 @@ class TestFileJpeg:
|
|||
with Image.open(filename):
|
||||
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):
|
||||
filename = "Tests/images/truncated_jpeg.jpg"
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
@ -442,7 +454,9 @@ class TestFileJpeg:
|
|||
with pytest.raises(OSError):
|
||||
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 _n_qtables_helper(n, test_file):
|
||||
with Image.open(test_file) as im:
|
||||
|
@ -452,7 +466,7 @@ class TestFileJpeg:
|
|||
assert len(im.quantization) == n
|
||||
reloaded = self.roundtrip(im, qtables="keep")
|
||||
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:
|
||||
qtables = im.quantization
|
||||
|
@ -464,7 +478,8 @@ class TestFileJpeg:
|
|||
|
||||
# valid bounds for baseline qtable
|
||||
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.
|
||||
standard_l_qtable = [
|
||||
|
@ -575,6 +590,12 @@ class TestFileJpeg:
|
|||
assert max(im2.quantization[0]) <= 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")
|
||||
def test_load_djpeg(self):
|
||||
with Image.open(TEST_FILE) as img:
|
||||
|
@ -647,15 +668,6 @@ class TestFileJpeg:
|
|||
reloaded.load()
|
||||
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):
|
||||
outfile = str(tmp_path / "temp.jpg")
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
|
@ -726,7 +738,9 @@ class TestFileJpeg:
|
|||
# OSError for unidentified image.
|
||||
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):
|
||||
with Image.open("Tests/images/flower.jpg") as im:
|
||||
exif = im.getexif()
|
||||
|
@ -757,7 +771,9 @@ class TestFileJpeg:
|
|||
# Act / Assert
|
||||
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):
|
||||
with Image.open("Tests/images/photoshop-200dpi.jpg") as im:
|
||||
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"]
|
||||
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):
|
||||
with Image.open("Tests/images/icc-after-SOF.jpg") as im:
|
||||
assert im.info["icc_profile"] == b"profile"
|
||||
|
@ -805,6 +835,32 @@ class TestFileJpeg:
|
|||
# Assert the entire file has not been read
|
||||
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")
|
||||
@skip_unless_feature("jpg")
|
||||
|
|