Merge branch 'master' into patch-3
|
@ -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\
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
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
|
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
|
||||||
|
|
2
.github/workflows/test-docker.yml
vendored
|
@ -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,
|
||||||
]
|
]
|
||||||
|
|
31
.github/workflows/test-windows.yml
vendored
|
@ -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\
|
||||||
|
|
||||||
|
|
7
.github/workflows/test.yml
vendored
|
@ -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
|
||||||
|
|
197
CHANGES.rst
|
@ -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]
|
||||||
|
|
||||||
|
|
7
Makefile
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
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
|
# Reproductions/tests for OOB read errors in FliDecode.c
|
||||||
|
|
||||||
|
|
|
@ -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",
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
import base64
|
import base64
|
||||||
import os
|
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)
|
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
|
||||||
|
|
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()
|
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__":
|
||||||
|
|
|
@ -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__":
|
||||||
|
|
|
@ -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.
|
||||||
|
|
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 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(
|
||||||
|
|
|
@ -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"):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|