diff --git a/.appveyor.yml b/.appveyor.yml index d525e4cfc..f86500b48 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -12,7 +12,7 @@ environment: matrix: - PYTHON: C:/Python310 ARCHITECTURE: x86 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python37-x64 ARCHITECTURE: x64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 656df5e91..2762d80c9 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -20,10 +20,12 @@ jobs: arch, centos-7-amd64, centos-stream-8-amd64, + centos-stream-9-amd64, debian-10-buster-x86, debian-11-bullseye-x86, fedora-34-amd64, fedora-35-amd64, + gentoo, ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, ] diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 8a9c1725d..51bd3a300 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -45,6 +45,7 @@ jobs: ${{ matrix.package }}-python-pyqt6 \ ${{ matrix.package }}-python3-setuptools \ ${{ matrix.package }}-freetype \ + ${{ matrix.package }}-gcc \ ${{ matrix.package }}-ghostscript \ ${{ matrix.package }}-lcms2 \ ${{ matrix.package }}-libimagequant \ diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index c78f9fd24..e2cf44cae 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -137,10 +137,11 @@ jobs: & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh - # failing with PyPy3 + # skip PyPy for speed - name: Enable heap verification if: "!contains(matrix.python-version, 'pypy')" - run: "& 'C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x86\\gflags.exe' /p /enable $env:pythonLocation\\python.exe" + run: | + & reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f - name: Test Pillow run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 414c7e94e..133972881 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,7 +91,7 @@ jobs: path: Tests/errors - name: Docs - if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.9 + if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10 run: | python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph make doccheck diff --git a/.readthedocs.yml b/.readthedocs.yml index 73e1f8213..0f581ebba 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,2 +1,8 @@ +version: 2 + python: - pip_install: true + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/CHANGES.rst b/CHANGES.rst index fc9455652..904a61ce1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,60 @@ Changelog (Pillow) 9.1.0 (unreleased) ------------------ +- Improved handling of PPM header #5121 + [Piolie, radarhere] + +- Reset size when seeking away from "Large Thumbnail" MPO frame #6101 + [radarhere] + +- Replace requirements.txt with extras #6072 + [hugovk, radarhere] + +- Added PyEncoder and support BLP saving #6069 + [radarhere] + +- Handle TGA images with packets that cross scan lines #6087 + [radarhere] + +- Added FITS reading #6056 + [radarhere, hugovk] + +- Added rawmode argument to Image.getpalette() #6061 + [radarhere] + +- Fixed BUFR, GRIB and HDF5 stub saving #6071 + [radarhere] + +- Do not automatically remove temporary ImageShow files on Unix #6045 + [radarhere] + +- Correctly read JPEG compressed BLP images #4685 + [Meithal, radarhere] + +- Merged _MODE_CONV typ into ImageMode as typestr #6057 + [radarhere] + +- Consider palette size when converting and in getpalette() #6060 + [radarhere] + +- Added enums #5954 + [radarhere] + +- Ensure image is opaque after converting P to PA with RGB palette #6052 + [radarhere] + +- Attach RGBA palettes from putpalette() when suitable #6054 + [radarhere] + +- Added get_photoshop_blocks() to parse Photoshop TIFF tag #6030 + [radarhere] + +- Drop excess values in BITSPERSAMPLE #6041 + [mikhail-iurkov] + +- Added unpacker from RGBA;15 to RGB #6031 + [radarhere] + - Enable arm64 for MSVC on Windows #5811 [gaborkertesz-linaro, gaborkertesz] @@ -23,7 +77,7 @@ Changelog (Pillow) - Ensure duplicated file pointer is closed #5946 [radarhere] -- Added specific error if ImagePath coordinate type is incorrect #5942 +- Added specific error if path coordinate type is incorrect #5942 [radarhere] - Return an empty bytestring from tobytes() for an empty image #5938 diff --git a/Makefile b/Makefile index 74a6a5ab2..263598599 100644 --- a/Makefile +++ b/Makefile @@ -9,9 +9,11 @@ clean: .PHONY: coverage coverage: - pytest -qq + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq rm -r htmlcov || true - coverage report + python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage + python3 -m coverage report .PHONY: doc doc: @@ -33,20 +35,16 @@ help: @echo "Welcome to Pillow development. Please use \`make \` where is one of" @echo " clean remove build products" @echo " coverage run coverage test (in progress)" - @echo " doc make html docs" - @echo " docserve run an http server on the docs directory" + @echo " doc make HTML docs" + @echo " docserve run an HTTP server on the docs directory" @echo " html to make standalone HTML files" @echo " inplace make inplace extension" @echo " install make and install" @echo " install-coverage make and install with C coverage" - @echo " install-req install documentation and test dependencies" - @echo " install-venv (deprecated) install in virtualenv" @echo " lint run the lint checks" - @echo " lint-fix run black and isort to (mostly) fix lint issues." + @echo " lint-fix run Black and isort to (mostly) fix lint issues" @echo " release-test run code and package tests before release" - @echo " test run tests on installed pillow" - @echo " upload build and upload sdists to PyPI" - @echo " upload-test build and upload sdists to test.pythonpackages.com" + @echo " test run tests on installed Pillow" .PHONY: inplace inplace: clean @@ -70,28 +68,17 @@ debug: make clean > /dev/null CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null -.PHONY: install-req -install-req: - python3 -m pip install -r requirements.txt - -.PHONY: install-venv -install-venv: - echo "'install-venv' is deprecated and will be removed in a future Pillow release" - virtualenv . - bin/pip install -r requirements.txt - .PHONY: release-test release-test: - $(MAKE) install-req - python3 -m pip install -e . + python3 -m pip install -e .[tests] python3 selftest.py python3 -m pytest Tests python3 -m pip install . -rm dist/*.egg -rmdir dist python3 -m pytest -qq - check-manifest - pyroma . + python3 -m check-manifest + python3 -m pyroma . $(MAKE) readme .PHONY: sdist @@ -101,26 +88,30 @@ sdist: .PHONY: test test: - pytest -qq + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq .PHONY: valgrind valgrind: - python3 -c "import pytest_valgrind" || python3 -m pip install pytest-valgrind + python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ --log-file=/tmp/valgrind-output \ python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output .PHONY: readme readme: - markdown2 README.md > .long-description.html && open .long-description.html + python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 + python3 -m markdown2 README.md > .long-description.html && open .long-description.html .PHONY: lint lint: - tox --help > /dev/null || python3 -m pip install tox - tox -e lint + python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox + python3 -m tox -e lint .PHONY: lint-fix lint-fix: - black --target-version py37 . - isort . + python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black + python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort + python3 -m black --target-version py37 . + python3 -m isort . diff --git a/Tests/images/blp/blp1_jpeg.png b/Tests/images/blp/blp1_jpeg.png new file mode 100644 index 000000000..be151f205 Binary files /dev/null and b/Tests/images/blp/blp1_jpeg.png differ diff --git a/Tests/images/cross_scan_line.png b/Tests/images/cross_scan_line.png new file mode 100644 index 000000000..64b68ed33 Binary files /dev/null and b/Tests/images/cross_scan_line.png differ diff --git a/Tests/images/cross_scan_line.tga b/Tests/images/cross_scan_line.tga new file mode 100644 index 000000000..5ef8c8154 Binary files /dev/null and b/Tests/images/cross_scan_line.tga differ diff --git a/Tests/images/hopper.fits b/Tests/images/hopper.fits index 85afa4ac1..7f28f75e5 100644 Binary files a/Tests/images/hopper.fits and b/Tests/images/hopper.fits differ diff --git a/Tests/images/pillow3.icns b/Tests/images/pillow3.icns index ef9b89178..49b691d90 100644 Binary files a/Tests/images/pillow3.icns and b/Tests/images/pillow3.icns differ diff --git a/Tests/images/tiff_wrong_bits_per_sample_2.tiff b/Tests/images/tiff_wrong_bits_per_sample_2.tiff new file mode 100644 index 000000000..d44176ce7 Binary files /dev/null and b/Tests/images/tiff_wrong_bits_per_sample_2.tiff differ diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 99e16391a..440bc325b 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,6 +1,5 @@ import os - -import pytest +import warnings from PIL import Image @@ -20,16 +19,14 @@ def test_bad(): either""" for f in get_files("b"): - with pytest.warns(None) as record: + # Assert that there is no unclosed file warning + with warnings.catch_warnings(): try: with Image.open(f) as im: im.load() except Exception: # as msg: pass - # Assert that there is no unclosed file warning - assert not record - def test_questionable(): """These shouldn't crash/dos, but it's not well defined that these diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 99776ce58..120ff777e 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -43,107 +43,158 @@ class TestColorLut3DCoreAPI: im = Image.new("RGB", (10, 10), 0) with pytest.raises(ValueError, match="filter"): - im.im.color_lut_3d("RGB", Image.CUBIC, *self.generate_identity_table(3, 3)) + im.im.color_lut_3d( + "RGB", Image.Resampling.BICUBIC, *self.generate_identity_table(3, 3) + ) with pytest.raises(ValueError, match="image mode"): im.im.color_lut_3d( - "wrong", Image.LINEAR, *self.generate_identity_table(3, 3) + "wrong", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) ) with pytest.raises(ValueError, match="table_channels"): - im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(5, 3)) - - with pytest.raises(ValueError, match="table_channels"): - im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(1, 3)) - - with pytest.raises(ValueError, match="table_channels"): - im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(2, 3)) - - with pytest.raises(ValueError, match="Table size"): im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (1, 3, 3)) + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(5, 3) + ) + + with pytest.raises(ValueError, match="table_channels"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(1, 3) + ) + + with pytest.raises(ValueError, match="table_channels"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(2, 3) ) with pytest.raises(ValueError, match="Table size"): im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (66, 3, 3)) + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (1, 3, 3)), + ) + + with pytest.raises(ValueError, match="Table size"): + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (66, 3, 3)), ) with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): - im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 7) + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7 + ) with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): - im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 9) + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9 + ) with pytest.raises(TypeError): - im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8) + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8 + ) with pytest.raises(TypeError): - im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, 16) + im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) def test_correct_args(self): im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(3, 3)) - - im.im.color_lut_3d("CMYK", Image.LINEAR, *self.generate_identity_table(4, 3)) - im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (2, 3, 3)) + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) ) im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (65, 3, 3)) + "CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ) im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (3, 65, 3)) + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (2, 3, 3)), ) im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (3, 3, 65)) + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (65, 3, 3)), + ) + + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (3, 65, 3)), + ) + + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (3, 3, 65)), ) def test_wrong_mode(self): with pytest.raises(ValueError, match="wrong mode"): im = Image.new("L", (10, 10), 0) - im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(3, 3)) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3)) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("L", (10, 10), 0) - im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3)) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("RGB", (10, 10), 0) im.im.color_lut_3d( - "RGBA", Image.LINEAR, *self.generate_identity_table(3, 3) + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) ) with pytest.raises(ValueError, match="wrong mode"): im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(4, 3)) + im.im.color_lut_3d( + "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("L", (10, 10), 0) + im.im.color_lut_3d( + "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) def test_correct_mode(self): im = Image.new("RGBA", (10, 10), 0) - im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(3, 3)) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) im = Image.new("RGBA", (10, 10), 0) - im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(4, 3)) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d("HSV", Image.LINEAR, *self.generate_identity_table(3, 3)) + im.im.color_lut_3d( + "HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(4, 3)) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) def test_identities(self): g = Image.linear_gradient("L") im = Image.merge( - "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], ) # Fast test with small cubes @@ -152,7 +203,9 @@ class TestColorLut3DCoreAPI: im, im._new( im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, size) + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, size), ) ), ) @@ -162,7 +215,9 @@ class TestColorLut3DCoreAPI: im, im._new( im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (2, 2, 65)) + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (2, 2, 65)), ) ), ) @@ -170,7 +225,12 @@ class TestColorLut3DCoreAPI: def test_identities_4_channels(self): g = Image.linear_gradient("L") im = Image.merge( - "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], ) # Red channel copied to alpha @@ -178,7 +238,9 @@ class TestColorLut3DCoreAPI: Image.merge("RGBA", (im.split() * 2)[:4]), im._new( im.im.color_lut_3d( - "RGBA", Image.LINEAR, *self.generate_identity_table(4, 17) + "RGBA", + Image.Resampling.BILINEAR, + *self.generate_identity_table(4, 17), ) ), ) @@ -189,9 +251,9 @@ class TestColorLut3DCoreAPI: "RGBA", [ g, - g.transpose(Image.ROTATE_90), - g.transpose(Image.ROTATE_180), - g.transpose(Image.ROTATE_270), + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + g.transpose(Image.Transpose.ROTATE_270), ], ) @@ -199,7 +261,9 @@ class TestColorLut3DCoreAPI: im, im._new( im.im.color_lut_3d( - "RGBA", Image.LINEAR, *self.generate_identity_table(3, 17) + "RGBA", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, 17), ) ), ) @@ -207,14 +271,19 @@ class TestColorLut3DCoreAPI: def test_channels_order(self): g = Image.linear_gradient("L") im = Image.merge( - "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], ) # Reverse channels by splitting and using table # fmt: off assert_image_equal( Image.merge('RGB', im.split()[::-1]), - im._new(im.im.color_lut_3d('RGB', Image.LINEAR, + im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, 3, 2, 2, 2, [ 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, @@ -227,11 +296,16 @@ class TestColorLut3DCoreAPI: def test_overflow(self): g = Image.linear_gradient("L") im = Image.merge( - "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], ) # fmt: off - transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR, + transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, 3, 2, 2, 2, [ -1, -1, -1, 2, -1, -1, @@ -251,7 +325,7 @@ class TestColorLut3DCoreAPI: assert transformed[205, 205] == (255, 255, 0) # fmt: off - transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR, + transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, 3, 2, 2, 2, [ -3, -3, -3, 5, -3, -3, @@ -354,7 +428,12 @@ class TestColorLut3DFilter: def test_numpy_formats(self): g = Image.linear_gradient("L") im = Image.merge( - "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], ) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) @@ -445,7 +524,12 @@ class TestGenerateColorLut3D: g = Image.linear_gradient("L") im = Image.merge( - "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], ) assert im == im.filter(lut) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index d48e5ce07..d1d5c85c1 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -120,9 +120,9 @@ def test_apng_dispose_op_previous_frame(): # save_all=True, # append_images=[green, blue], # disposal=[ - # PngImagePlugin.APNG_DISPOSE_OP_NONE, - # PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, - # PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS + # PngImagePlugin.Disposal.OP_NONE, + # PngImagePlugin.Disposal.OP_PREVIOUS, + # PngImagePlugin.Disposal.OP_PREVIOUS # ], # ) with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: @@ -455,31 +455,31 @@ def test_apng_save_disposal(tmp_path): green = Image.new("RGBA", size, (0, 255, 0, 255)) transparent = Image.new("RGBA", size, (0, 0, 0, 0)) - # test APNG_DISPOSE_OP_NONE + # test OP_NONE red.save( test_file, save_all=True, append_images=[green, transparent], - disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, + disposal=PngImagePlugin.Disposal.OP_NONE, + blend=PngImagePlugin.Blend.OP_OVER, ) with Image.open(test_file) as im: im.seek(2) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) - # test APNG_DISPOSE_OP_BACKGROUND + # test OP_BACKGROUND disposal = [ - PngImagePlugin.APNG_DISPOSE_OP_NONE, - PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, - PngImagePlugin.APNG_DISPOSE_OP_NONE, + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_BACKGROUND, + PngImagePlugin.Disposal.OP_NONE, ] red.save( test_file, save_all=True, append_images=[red, transparent], disposal=disposal, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, + blend=PngImagePlugin.Blend.OP_OVER, ) with Image.open(test_file) as im: im.seek(2) @@ -487,26 +487,26 @@ def test_apng_save_disposal(tmp_path): assert im.getpixel((64, 32)) == (0, 0, 0, 0) disposal = [ - PngImagePlugin.APNG_DISPOSE_OP_NONE, - PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_BACKGROUND, ] red.save( test_file, save_all=True, append_images=[green], disposal=disposal, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, + blend=PngImagePlugin.Blend.OP_OVER, ) with Image.open(test_file) as im: im.seek(1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) - # test APNG_DISPOSE_OP_PREVIOUS + # test OP_PREVIOUS disposal = [ - PngImagePlugin.APNG_DISPOSE_OP_NONE, - PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, - PngImagePlugin.APNG_DISPOSE_OP_NONE, + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_PREVIOUS, + PngImagePlugin.Disposal.OP_NONE, ] red.save( test_file, @@ -514,7 +514,7 @@ def test_apng_save_disposal(tmp_path): append_images=[green, red, transparent], default_image=True, disposal=disposal, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, + blend=PngImagePlugin.Blend.OP_OVER, ) with Image.open(test_file) as im: im.seek(3) @@ -522,15 +522,15 @@ def test_apng_save_disposal(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) disposal = [ - PngImagePlugin.APNG_DISPOSE_OP_NONE, - PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_PREVIOUS, ] red.save( test_file, save_all=True, append_images=[green], disposal=disposal, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, + blend=PngImagePlugin.Blend.OP_OVER, ) with Image.open(test_file) as im: im.seek(1) @@ -538,7 +538,7 @@ def test_apng_save_disposal(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) # test info disposal - red.info["disposal"] = PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND + red.info["disposal"] = PngImagePlugin.Disposal.OP_BACKGROUND red.save( test_file, save_all=True, @@ -556,12 +556,12 @@ def test_apng_save_disposal_previous(tmp_path): red = Image.new("RGBA", size, (255, 0, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255)) - # test APNG_DISPOSE_OP_NONE + # test OP_NONE transparent.save( test_file, save_all=True, append_images=[red, green], - disposal=PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, + disposal=PngImagePlugin.Disposal.OP_PREVIOUS, ) with Image.open(test_file) as im: im.seek(2) @@ -576,17 +576,17 @@ def test_apng_save_blend(tmp_path): green = Image.new("RGBA", size, (0, 255, 0, 255)) transparent = Image.new("RGBA", size, (0, 0, 0, 0)) - # test APNG_BLEND_OP_SOURCE on solid color + # test OP_SOURCE on solid color blend = [ - PngImagePlugin.APNG_BLEND_OP_OVER, - PngImagePlugin.APNG_BLEND_OP_SOURCE, + PngImagePlugin.Blend.OP_OVER, + PngImagePlugin.Blend.OP_SOURCE, ] red.save( test_file, save_all=True, append_images=[red, green], default_image=True, - disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, + disposal=PngImagePlugin.Disposal.OP_NONE, blend=blend, ) with Image.open(test_file) as im: @@ -594,17 +594,17 @@ def test_apng_save_blend(tmp_path): assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) - # test APNG_BLEND_OP_SOURCE on transparent color + # test OP_SOURCE on transparent color blend = [ - PngImagePlugin.APNG_BLEND_OP_OVER, - PngImagePlugin.APNG_BLEND_OP_SOURCE, + PngImagePlugin.Blend.OP_OVER, + PngImagePlugin.Blend.OP_SOURCE, ] red.save( test_file, save_all=True, append_images=[red, transparent], default_image=True, - disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, + disposal=PngImagePlugin.Disposal.OP_NONE, blend=blend, ) with Image.open(test_file) as im: @@ -612,14 +612,14 @@ def test_apng_save_blend(tmp_path): assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) - # test APNG_BLEND_OP_OVER + # test OP_OVER red.save( test_file, save_all=True, append_images=[green, transparent], default_image=True, - disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, + disposal=PngImagePlugin.Disposal.OP_NONE, + blend=PngImagePlugin.Blend.OP_OVER, ) with Image.open(test_file) as im: im.seek(1) @@ -630,8 +630,18 @@ def test_apng_save_blend(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) # test info blend - red.info["blend"] = PngImagePlugin.APNG_BLEND_OP_OVER + red.info["blend"] = PngImagePlugin.Blend.OP_OVER red.save(test_file, save_all=True, append_images=[green, transparent]) with Image.open(test_file) as im: im.seek(2) assert im.getpixel((0, 0)) == (0, 255, 0, 255) + + +def test_constants_deprecation(): + for enum, prefix in { + PngImagePlugin.Disposal: "APNG_DISPOSE_", + PngImagePlugin.Blend: "APNG_BLEND_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(PngImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 15bd7e4f8..c1fae44ca 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,8 +1,18 @@ import pytest -from PIL import Image +from PIL import BlpImagePlugin, Image -from .helper import assert_image_equal_tofile +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, +) + + +def test_load_blp1(): + with Image.open("Tests/images/blp/blp1_jpeg.blp") as im: + assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") def test_load_blp2_raw(): @@ -20,6 +30,28 @@ def test_load_blp2_dxt1a(): assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") +def test_save(tmp_path): + f = str(tmp_path / "temp.blp") + + for version in ("BLP1", "BLP2"): + im = hopper("P") + im.save(f, blp_version=version) + + with Image.open(f) as reloaded: + assert_image_equal(im.convert("RGB"), reloaded) + + with Image.open("Tests/images/transparent.png") as im: + f = str(tmp_path / "temp.blp") + im.convert("P").save(f, blp_version=version) + + with Image.open(f) as reloaded: + assert_image_similar(im, reloaded, 8) + + im = hopper() + with pytest.raises(ValueError): + im.save(f) + + @pytest.mark.parametrize( "test_file", [ @@ -37,3 +69,14 @@ def test_crashes(test_file): with Image.open(f) as im: with pytest.raises(OSError): im.load() + + +def test_constants_deprecation(): + for enum, prefix in { + BlpImagePlugin.Format: "BLP_FORMAT_", + BlpImagePlugin.Encoding: "BLP_ENCODING_", + BlpImagePlugin.AlphaEncoding: "BLP_ALPHA_ENCODING_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(BlpImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 11acc1c88..e330404d6 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -45,3 +45,35 @@ def test_save(tmp_path): # Act / Assert: stub cannot save without an implemented handler with pytest.raises(OSError): im.save(tmpfile) + + +def test_handler(tmp_path): + class TestHandler: + opened = False + loaded = False + saved = False + + def open(self, im): + self.opened = True + + def load(self, im): + self.loaded = True + return Image.new("RGB", (1, 1)) + + def save(self, im, fp, filename): + self.saved = True + + handler = TestHandler() + BufrStubImagePlugin.register_handler(handler) + with Image.open(TEST_FILE) as im: + assert handler.opened + assert not handler.loaded + + im.load() + assert handler.loaded + + temp_file = str(tmp_path / "temp.bufr") + im.save(temp_file) + assert handler.saved + + BufrStubImagePlugin._handler = None diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 58d5cbf1a..0f09c4b99 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import DcxImagePlugin, Image @@ -31,21 +33,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() - assert not record - def test_invalid_file(): with open("Tests/images/flower.jpg", "rb") as fp: diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py new file mode 100644 index 000000000..447888acd --- /dev/null +++ b/Tests/test_file_fits.py @@ -0,0 +1,80 @@ +from io import BytesIO + +import pytest + +from PIL import FitsImagePlugin, FitsStubImagePlugin, Image + +from .helper import assert_image_equal, hopper + +TEST_FILE = "Tests/images/hopper.fits" + + +def test_open(): + # Act + with Image.open(TEST_FILE) as im: + + # Assert + assert im.format == "FITS" + assert im.size == (128, 128) + assert im.mode == "L" + + assert_image_equal(im, hopper("L")) + + +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" + + # Act / Assert + with pytest.raises(SyntaxError): + FitsImagePlugin.FitsImageFile(invalid_file) + + +def test_truncated_fits(): + # No END to headers + image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE" + with pytest.raises(OSError): + FitsImagePlugin.FitsImageFile(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_stub_deprecated(): + class Handler: + opened = False + loaded = False + + def open(self, im): + self.opened = True + + def load(self, im): + self.loaded = True + return Image.new("RGB", (1, 1)) + + handler = Handler() + with pytest.warns(DeprecationWarning): + FitsStubImagePlugin.register_handler(handler) + + with Image.open(TEST_FILE) as im: + assert im.format == "FITS" + assert im.size == (128, 128) + assert im.mode == "L" + + assert handler.opened + assert not handler.loaded + + im.load() + assert handler.loaded + + FitsStubImagePlugin._handler = None + Image.register_open( + FitsImagePlugin.FitsImageFile.format, + FitsImagePlugin.FitsImageFile, + FitsImagePlugin._accept, + ) diff --git a/Tests/test_file_fitsstub.py b/Tests/test_file_fitsstub.py deleted file mode 100644 index c77457947..000000000 --- a/Tests/test_file_fitsstub.py +++ /dev/null @@ -1,63 +0,0 @@ -from io import BytesIO - -import pytest - -from PIL import FitsStubImagePlugin, Image - -TEST_FILE = "Tests/images/hopper.fits" - - -def test_open(): - # Act - with Image.open(TEST_FILE) as im: - - # Assert - assert im.format == "FITS" - assert im.size == (128, 128) - assert im.mode == "L" - - -def test_invalid_file(): - # Arrange - invalid_file = "Tests/images/flower.jpg" - - # Act / Assert - with pytest.raises(SyntaxError): - FitsStubImagePlugin.FITSStubImageFile(invalid_file) - - -def test_load(): - # Arrange - with Image.open(TEST_FILE) as im: - - # Act / Assert: stub cannot load without an implemented handler - with pytest.raises(OSError): - 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: - dummy_fp = None - dummy_filename = "dummy.filename" - - # Act / Assert: stub cannot save without an implemented handler - with pytest.raises(OSError): - im.save(dummy_filename) - with pytest.raises(OSError): - FitsStubImagePlugin._save(im, dummy_fp, dummy_filename) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 675e06bf8..c1ad4a7f0 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import FliImagePlugin, Image @@ -38,21 +40,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(static_test_file) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(static_test_file) as im: im.load() - assert not record - def test_tell(): # Arrange diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index f76fd895a..5447dc740 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,4 +1,6 @@ -from PIL import Image +import pytest + +from PIL import FtexImagePlugin, Image from .helper import assert_image_equal_tofile, assert_image_similar @@ -12,3 +14,12 @@ def test_load_dxt1(): with Image.open("Tests/images/ftex_dxt1.ftc") as im: with Image.open("Tests/images/ftex_dxt1.png") as target: assert_image_similar(im, target.convert("RGBA"), 15) + + +def test_constants_deprecation(): + for enum, prefix in { + FtexImagePlugin.Format: "FORMAT_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(FtexImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 00bf582fa..011d982f0 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,3 +1,4 @@ +import warnings from io import BytesIO import pytest @@ -39,21 +40,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(TEST_GIF) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(TEST_GIF) as im: im.load() - assert not record - def test_invalid_file(): invalid_file = "Tests/images/flower.jpg" @@ -210,8 +207,8 @@ def test_palette_handling(tmp_path): with Image.open(TEST_GIF) as im: im = im.convert("RGB") - im = im.resize((100, 100), Image.LANCZOS) - im2 = im.convert("P", palette=Image.ADAPTIVE, colors=256) + im = im.resize((100, 100), Image.Resampling.LANCZOS) + im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) f = str(tmp_path / "temp.gif") im2.save(f, optimize=True) @@ -311,6 +308,22 @@ def test_n_frames(): assert im.is_animated == (n_frames != 1) +def test_no_change(): + # Test n_frames does not change the image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + im.seek(1) + expected = im.copy() + assert im.n_frames == 5 + assert_image_equal(im, expected) + + # Test is_animated does not change the image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + im.seek(3) + expected = im.copy() + assert im.is_animated + assert_image_equal(im, expected) + + def test_eoferror(): with Image.open(TEST_GIF) as im: n_frames = im.n_frames @@ -911,7 +924,7 @@ def test_save_I(tmp_path): def test_getdata(): # Test getheader/getdata against legacy values. # Create a 'P' image with holes in the palette. - im = Image._wedge().resize((16, 16), Image.NEAREST) + im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) im.putpalette(ImagePalette.ImagePalette("RGB")) im.info = {"background": 0} diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index e4930d8dc..fd427746e 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -45,3 +45,35 @@ def test_save(tmp_path): # Act / Assert: stub cannot save without an implemented handler with pytest.raises(OSError): im.save(tmpfile) + + +def test_handler(tmp_path): + class TestHandler: + opened = False + loaded = False + saved = False + + def open(self, im): + self.opened = True + + def load(self, im): + self.loaded = True + return Image.new("RGB", (1, 1)) + + def save(self, im, fp, filename): + self.saved = True + + handler = TestHandler() + GribStubImagePlugin.register_handler(handler) + with Image.open(TEST_FILE) as im: + assert handler.opened + assert not handler.loaded + + im.load() + assert handler.loaded + + temp_file = str(tmp_path / "temp.grib") + im.save(temp_file) + assert handler.saved + + GribStubImagePlugin._handler = None diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index ff3397055..20b4b9619 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -46,3 +46,35 @@ def test_save(): im.save(dummy_filename) with pytest.raises(OSError): Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename) + + +def test_handler(tmp_path): + class TestHandler: + opened = False + loaded = False + saved = False + + def open(self, im): + self.opened = True + + def load(self, im): + self.loaded = True + return Image.new("RGB", (1, 1)) + + def save(self, im, fp, filename): + self.saved = True + + handler = TestHandler() + Hdf5StubImagePlugin.register_handler(handler) + with Image.open(TEST_FILE) as im: + assert handler.opened + assert not handler.loaded + + im.load() + assert handler.loaded + + temp_file = str(tmp_path / "temp.h5") + im.save(temp_file) + assert handler.saved + + Hdf5StubImagePlugin._handler = None diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index b492f6cb2..7d8f89184 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,5 +1,6 @@ import io import os +import warnings import pytest @@ -19,9 +20,8 @@ def test_sanity(): with Image.open(TEST_FILE) as im: # Assert that there is no unclosed file warning - with pytest.warns(None) as record: + with warnings.catch_warnings(): im.load() - assert not record assert im.mode == "RGBA" assert im.size == (1024, 1024) @@ -112,12 +112,9 @@ def test_older_icon(): def test_jp2_icon(): - # This icon was made by using Uli Kusterer's oldiconutil to replace - # the PNG images with JPEG 2000 ones. The advantage of doing this is - # that OS X 10.5 supports JPEG 2000 but not PNG; some commercial - # software therefore does just this. - - # (oldiconutil is here: https://github.com/uliwitness/oldiconutil) + # This icon uses JPEG 2000 images instead of the PNG images. + # The advantage of doing this is that OS X 10.5 supports JPEG 2000 + # but not PNG; some commercial software therefore does just this. if not ENABLE_JPEG2K: return diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 73ac6f742..12b80fbde 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -53,7 +53,9 @@ def test_save_to_bytes(): assert im.mode == reloaded.mode assert (64, 64) == reloaded.size assert reloaded.format == "ICO" - assert_image_equal(reloaded, hopper().resize((64, 64), Image.LANCZOS)) + assert_image_equal( + reloaded, hopper().resize((64, 64), Image.Resampling.LANCZOS) + ) # The other one output.seek(0) @@ -63,7 +65,9 @@ def test_save_to_bytes(): assert im.mode == reloaded.mode assert (32, 32) == reloaded.size assert reloaded.format == "ICO" - assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) + assert_image_equal( + reloaded, hopper().resize((32, 32), Image.Resampling.LANCZOS) + ) @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) @@ -80,7 +84,7 @@ def test_save_to_bytes_bmp(mode): assert "RGBA" == reloaded.mode assert (64, 64) == reloaded.size assert reloaded.format == "ICO" - im = hopper(mode).resize((64, 64), Image.LANCZOS).convert("RGBA") + im = hopper(mode).resize((64, 64), Image.Resampling.LANCZOS).convert("RGBA") assert_image_equal(reloaded, im) # The other one @@ -91,7 +95,7 @@ def test_save_to_bytes_bmp(mode): assert "RGBA" == reloaded.mode assert (32, 32) == reloaded.size assert reloaded.format == "ICO" - im = hopper(mode).resize((32, 32), Image.LANCZOS).convert("RGBA") + im = hopper(mode).resize((32, 32), Image.Resampling.LANCZOS).convert("RGBA") assert_image_equal(reloaded, im) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 9d25a4d1a..675210c30 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,4 +1,5 @@ import filecmp +import warnings import pytest @@ -35,21 +36,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(TEST_IM) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(TEST_IM) as im: im.load() - assert not record - def test_tell(): # Arrange diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 4b2ffe70d..e5e8c85f4 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,5 +1,6 @@ import os import re +import warnings from io import BytesIO import pytest @@ -271,7 +272,7 @@ class TestFileJpeg: del exif[0x8769] # Assert that it needs to be transposed - assert exif[0x0112] == Image.TRANSVERSE + assert exif[0x0112] == Image.Transpose.TRANSVERSE # Assert that the GPS IFD is present and empty assert exif.get_ifd(0x8825) == {} @@ -756,9 +757,8 @@ class TestFileJpeg: assert exif[282] == 180 out = str(tmp_path / "out.jpg") - with pytest.warns(None) as record: + with warnings.catch_warnings(): im.save(out, exif=exif) - assert not record with Image.open(out) as reloaded: assert reloaded.getexif()[282] == 180 diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index ca410162a..b5ea6d0a0 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -291,7 +291,7 @@ def test_subsampling_decode(name): # RGB reference images are downscaled epsilon = 3e-3 width, height = width * 2, height * 2 - expected = im2.resize((width, height), Image.NEAREST) + expected = im2.resize((width, height), Image.Resampling.NEAREST) assert_image_similar(im, expected, epsilon) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index e40a19394..53ed2520a 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -112,7 +112,7 @@ class TestFileLibTiff(LibTiffTestCase): test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: out = str(tmp_path / "temp.tif") - rot = orig.transpose(Image.ROTATE_90) + rot = orig.transpose(Image.Transpose.ROTATE_90) assert rot.size == (500, 500) rot.save(out) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 9de096458..0fa3b6382 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,3 +1,4 @@ +import warnings from io import BytesIO import pytest @@ -41,21 +42,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(test_files[0]) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(test_files[0]) as im: im.load() - assert not record - def test_app(): for test_file in test_files: @@ -88,6 +85,9 @@ def test_frame_size(): im.seek(1) assert im.size == (680, 480) + im.seek(0) + assert im.size == (640, 480) + def test_ignore_frame_size(): # Ignore the different size of the second frame diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0869cc58b..bb2b0d119 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,5 +1,6 @@ import re import sys +import warnings import zlib from io import BytesIO @@ -331,9 +332,8 @@ class TestFilePng: with Image.open(TEST_PNG_FILE) as im: # Assert that there is no unclosed file warning - with pytest.warns(None) as record: + with warnings.catch_warnings(): im.verify() - assert not record with Image.open(TEST_PNG_FILE) as im: im.load() diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index ad36319db..0e4f1ba68 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -3,7 +3,7 @@ from io import BytesIO import pytest -from PIL import Image +from PIL import Image, UnidentifiedImageError from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -50,15 +50,70 @@ def test_pnm(tmp_path): assert_image_equal_tofile(im, f) +def test_magic(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"PyInvalid") + + with pytest.raises(UnidentifiedImageError): + with Image.open(path): + pass + + +def test_header_with_comments(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") + + with Image.open(path) as im: + assert im.size == (128, 128) + + +def test_non_integer_token(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6\nTEST") + + with pytest.raises(ValueError): + with Image.open(path): + pass + + +def test_token_too_long(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6\n 01234567890") + + with pytest.raises(ValueError) as e: + with Image.open(path): + pass + + assert str(e.value) == "Token too long in file header: b'01234567890'" + + +def test_too_many_colors(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6\n1 1\n1000\n") + + with pytest.raises(ValueError) as e: + with Image.open(path): + pass + + assert str(e.value) == "Too many colors for band: 1000" + + def test_truncated_file(tmp_path): path = str(tmp_path / "temp.pgm") with open(path, "w") as f: f.write("P6") - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: with Image.open(path): pass + assert str(e.value) == "Reached EOF while reading header" + def test_neg_ppm(): # Storage.c accepted negative values for xsize, ysize. the diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index a7f379e55..b4b5b7a0c 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import Image, PsdImagePlugin @@ -29,21 +31,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(test_file) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(test_file) as im: im.load() - assert not record - def test_invalid_file(): invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 3c93160f1..0e3b705a2 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,4 +1,5 @@ import tempfile +import warnings from io import BytesIO import pytest @@ -28,21 +29,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() - assert not record - def test_save(tmp_path): # Arrange diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index b38727fb9..5daab47fc 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import Image, TarIO, features @@ -31,16 +33,12 @@ def test_unclosed_file(): def test_close(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar.close() - assert not record - def test_contextmanager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): pass - - assert not record diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index e2351d723..aeea3fb42 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -97,6 +97,11 @@ def test_id_field_rle(): assert im.size == (199, 199) +def test_cross_scan_line(): + with Image.open("Tests/images/cross_scan_line.tga") as im: + assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png") + + def test_save(tmp_path): test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 5801e1766..28aeff075 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,4 +1,5 @@ import os +import warnings from io import BytesIO import pytest @@ -64,20 +65,16 @@ class TestFileTiff: pytest.warns(ResourceWarning, open) def test_closed_file(self): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open("Tests/images/multipage.tiff") im.load() im.close() - assert not record - def test_context_manager(self): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open("Tests/images/multipage.tiff") as im: im.load() - assert not record - def test_mac_tiff(self): # Read RGBa images from macOS [@PIL136] @@ -90,11 +87,18 @@ class TestFileTiff: assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - def test_wrong_bits_per_sample(self): - with Image.open("Tests/images/tiff_wrong_bits_per_sample.tiff") as im: - assert im.mode == "RGBA" - assert im.size == (52, 53) - assert im.tile == [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))] + @pytest.mark.parametrize( + "file_name,mode,size,offset", + [ + ("tiff_wrong_bits_per_sample.tiff", "RGBA", (52, 53), 160), + ("tiff_wrong_bits_per_sample_2.tiff", "RGB", (16, 16), 8), + ], + ) + def test_wrong_bits_per_sample(self, file_name, mode, size, offset): + with Image.open("Tests/images/" + file_name) as im: + assert im.mode == mode + assert im.size == size + assert im.tile == [("raw", (0, 0) + size, offset, (mode, 0, 1))] im.load() def test_set_legacy_api(self): @@ -685,6 +689,32 @@ class TestFileTiff: assert description[0]["format"] == "image/tiff" assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] + def test_get_photoshop_blocks(self): + with Image.open("Tests/images/lab.tif") as im: + assert list(im.get_photoshop_blocks().keys()) == [ + 1061, + 1002, + 1005, + 1062, + 1037, + 1049, + 1011, + 1034, + 10000, + 1013, + 1016, + 1032, + 1054, + 1050, + 1064, + 1041, + 1044, + 1036, + 1057, + 4000, + 4001, + ] + def test_close_on_load_exclusive(self, tmp_path): # similar to test_fd_leak, but runs on unixlike os tmpfile = str(tmp_path / "temp.tif") diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index e72b4993c..55897f1eb 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,6 +1,7 @@ import io import re import sys +import warnings import pytest @@ -161,9 +162,8 @@ class TestFileWebp: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: temp_file = str(tmp_path / "temp.webp") - with pytest.warns(None) as record: + with warnings.catch_warnings(): image.save(temp_file) - assert not record def test_file_pointer_could_be_reused(self): file_path = "Tests/images/hopper.webp" diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 3b9c8b071..b485e854f 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -77,7 +77,7 @@ def to_rgb_colorsys(im): def test_wedge(): - src = wedge().resize((3 * 32, 32), Image.BILINEAR) + src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR) im = src.convert("HSV") comparable = to_hsv_colorsys(src) diff --git a/Tests/test_image.py b/Tests/test_image.py index 31b286d31..2cd858df1 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -3,6 +3,7 @@ import os import shutil import sys import tempfile +import warnings import pytest @@ -648,9 +649,8 @@ class TestImage: # Act/Assert with Image.open(test_file) as im: - with pytest.warns(None) as record: + with warnings.catch_warnings(): im.save(temp_file) - assert not record def test_load_on_nonexclusive_multiframe(self): with open("Tests/images/frozenpond.mpo", "rb") as fp: @@ -813,6 +813,31 @@ class TestImage: with pytest.warns(DeprecationWarning): assert Image.CONTAINER == 2 + def test_constants_deprecation(self): + with pytest.warns(DeprecationWarning): + assert Image.NEAREST == 0 + with pytest.warns(DeprecationWarning): + assert Image.NONE == 0 + + with pytest.warns(DeprecationWarning): + assert Image.LINEAR == Image.Resampling.BILINEAR + with pytest.warns(DeprecationWarning): + assert Image.CUBIC == Image.Resampling.BICUBIC + with pytest.warns(DeprecationWarning): + assert Image.ANTIALIAS == Image.Resampling.LANCZOS + + for enum in ( + Image.Transpose, + Image.Transform, + Image.Resampling, + Image.Dither, + Image.Palette, + Image.Quantize, + ): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(Image, name) == enum[name] + @pytest.mark.parametrize( "path", [ diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index a5a95e962..1d6469819 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -76,6 +76,13 @@ def test_16bit_workaround(): _test_float_conversion(im.convert("I")) +def test_opaque(): + alpha = hopper("P").convert("PA").getchannel("A") + + solid = Image.new("L", (128, 128), 255) + assert_image_equal(alpha, solid) + + def test_rgba_p(): im = hopper("RGBA") im.putalpha(hopper("L")) @@ -136,7 +143,7 @@ def test_trns_l(tmp_path): assert "transparency" in im_p.info im_p.save(f) - im_p = im.convert("P", palette=Image.ADAPTIVE) + im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) assert "transparency" in im_p.info im_p.save(f) @@ -159,13 +166,13 @@ def test_trns_RGB(tmp_path): assert "transparency" not in im_rgba.info im_rgba.save(f) - im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) + im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE) assert "transparency" not in im_p.info im_p.save(f) im = Image.new("RGB", (1, 1)) im.info["transparency"] = im.getpixel((0, 0)) - im_p = im.convert("P", palette=Image.ADAPTIVE) + im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) assert im_p.info["transparency"] == im_p.getpixel((0, 0)) im_p.save(f) diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index 159efd78a..36c81b40f 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -14,7 +14,7 @@ def test_sanity(): def test_roundtrip(): def getdata(mode): - im = hopper(mode).resize((32, 30), Image.NEAREST) + im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) data = im.getdata() return data[0], len(data), len(list(data)) diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index 1818adca2..58a6dacbb 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -1,3 +1,5 @@ +from PIL import Image + from .helper import hopper @@ -17,3 +19,26 @@ def test_palette(): assert palette("RGBA") is None assert palette("CMYK") is None assert palette("YCbCr") is None + + +def test_palette_rawmode(): + im = Image.new("P", (1, 1)) + im.putpalette((1, 2, 3)) + + for rawmode in ("RGB", None): + rgb = im.getpalette(rawmode) + assert rgb == [1, 2, 3] + + # Convert the RGB palette to RGBA + rgba = im.getpalette("RGBA") + assert rgba == [1, 2, 3, 255] + + im.putpalette((1, 2, 3, 4), "RGBA") + + # Convert the RGBA palette to RGB + rgb = im.getpalette("RGB") + assert rgb == [1, 2, 3] + + for rawmode in ("RGBA", None): + rgba = im.getpalette(rawmode) + assert rgba == [1, 2, 3, 4] diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 0232a5536..670b2f4eb 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -21,6 +21,7 @@ def test_sanity(): assert m.bands == ("1",) assert m.basemode == "L" assert m.basetype == "L" + assert m.typestr == "|b1" for mode in ( "I;16", @@ -45,6 +46,7 @@ def test_sanity(): assert m.bands == ("R", "G", "B") assert m.basemode == "RGB" assert m.basetype == "L" + assert m.typestr == "|u1" def test_properties(): diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 1d3ca8135..dc3caef01 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -45,7 +45,7 @@ class TestImagingPaste: @cached_property def mask_L(self): - return self.gradient_L.transpose(Image.ROTATE_270) + return self.gradient_L.transpose(Image.Transpose.ROTATE_270) @cached_property def gradient_L(self): @@ -62,8 +62,8 @@ class TestImagingPaste: "RGB", [ self.gradient_L, - self.gradient_L.transpose(Image.ROTATE_90), - self.gradient_L.transpose(Image.ROTATE_180), + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + self.gradient_L.transpose(Image.Transpose.ROTATE_180), ], ) @@ -73,9 +73,9 @@ class TestImagingPaste: "RGBA", [ self.gradient_L, - self.gradient_L.transpose(Image.ROTATE_90), - self.gradient_L.transpose(Image.ROTATE_180), - self.gradient_L.transpose(Image.ROTATE_270), + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + self.gradient_L.transpose(Image.Transpose.ROTATE_180), + self.gradient_L.transpose(Image.Transpose.ROTATE_270), ], ) @@ -85,9 +85,9 @@ class TestImagingPaste: "RGBa", [ self.gradient_L, - self.gradient_L.transpose(Image.ROTATE_90), - self.gradient_L.transpose(Image.ROTATE_180), - self.gradient_L.transpose(Image.ROTATE_270), + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + self.gradient_L.transpose(Image.Transpose.ROTATE_180), + self.gradient_L.transpose(Image.Transpose.ROTATE_270), ], ) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 012a57a09..3b29769a7 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -62,3 +62,17 @@ def test_putpalette_with_alpha_values(): im.putpalette(palette_with_alpha_values, "RGBA") assert_image_equal(im.convert("RGBA"), expected) + + +@pytest.mark.parametrize( + "mode, palette", + ( + ("RGBA", (1, 2, 3, 4)), + ("RGBAX", (1, 2, 3, 4, 0)), + ), +) +def test_rgba_palette(mode, palette): + im = Image.new("P", (1, 1)) + im.putpalette(palette, mode) + assert im.getpalette() == [1, 2, 3] + assert im.palette.colors == {(1, 2, 3, 4): 0} diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 53b6c9007..e9afd9118 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,8 +1,9 @@ import pytest +from packaging.version import parse as parse_version -from PIL import Image +from PIL import Image, features -from .helper import assert_image_similar, hopper, is_ppc64le +from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature def test_sanity(): @@ -17,16 +18,14 @@ def test_sanity(): assert_image_similar(converted.convert("RGB"), image, 60) -@pytest.mark.xfail(is_ppc64le(), reason="failing on ppc64le on GHA") +@skip_unless_feature("libimagequant") def test_libimagequant_quantize(): image = hopper() - try: - converted = image.quantize(100, Image.LIBIMAGEQUANT) - except ValueError as ex: # pragma: no cover - if "dependency" in str(ex).lower(): - pytest.skip("libimagequant support not available") - else: - raise + if is_ppc64le(): + libimagequant = parse_version(features.version_feature("libimagequant")) + if libimagequant < parse_version("4"): + pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") + converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 15) assert len(converted.getcolors()) == 100 @@ -34,7 +33,7 @@ def test_libimagequant_quantize(): def test_octree_quantize(): image = hopper() - converted = image.quantize(100, Image.FASTOCTREE) + converted = image.quantize(100, Image.Quantize.FASTOCTREE) assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 20) assert len(converted.getcolors()) == 100 @@ -61,7 +60,7 @@ def test_quantize_no_dither(): with Image.open("Tests/images/caption_6_33_22.png") as palette: palette = palette.convert("P") - converted = image.quantize(dither=0, palette=palette) + converted = image.quantize(dither=Image.Dither.NONE, palette=palette) assert converted.mode == "P" assert converted.palette.palette == palette.palette.palette @@ -71,8 +70,8 @@ def test_quantize_dither_diff(): with Image.open("Tests/images/caption_6_33_22.png") as palette: palette = palette.convert("P") - dither = image.quantize(dither=1, palette=palette) - nodither = image.quantize(dither=0, palette=palette) + dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette) + nodither = image.quantize(dither=Image.Dither.NONE, palette=palette) assert dither.tobytes() != nodither.tobytes() @@ -97,10 +96,10 @@ def test_transparent_colors_equal(): @pytest.mark.parametrize( "method, color", ( - (Image.MEDIANCUT, (0, 0, 0)), - (Image.MAXCOVERAGE, (0, 0, 0)), - (Image.FASTOCTREE, (0, 0, 0)), - (Image.FASTOCTREE, (0, 0, 0, 0)), + (Image.Quantize.MEDIANCUT, (0, 0, 0)), + (Image.Quantize.MAXCOVERAGE, (0, 0, 0)), + (Image.Quantize.FASTOCTREE, (0, 0, 0)), + (Image.Quantize.FASTOCTREE, (0, 0, 0, 0)), ), ) def test_palette(method, color): @@ -109,3 +108,18 @@ def test_palette(method, color): converted = im.quantize(method=method) converted_px = converted.load() assert converted_px[0, 0] == converted.palette.colors[color] + + +def test_small_palette(): + # Arrange + im = hopper() + + colors = (255, 0, 0, 0, 0, 255) + p = Image.new("P", (1, 1)) + p.putpalette(colors) + + # Act + im = im.quantize(palette=p) + + # Assert + assert len(im.getcolors()) == 2 diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index b4eebc142..70dc87f0a 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -97,7 +97,7 @@ def get_image(mode): bands = [gradients_image] for _ in mode_info.bands[1:]: # rotate previous image - band = bands[-1].transpose(Image.ROTATE_90) + band = bands[-1].transpose(Image.Transpose.ROTATE_90) bands.append(band) # Correct alpha channel by transforming completely transparent pixels. # Low alpha values also emphasize error after alpha multiplication. @@ -138,24 +138,26 @@ def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1): reference = Image.new(im.mode, reduced.size) area_size = (im.size[0] // factor[0], im.size[1] // factor[1]) area_box = (0, 0, area_size[0] * factor[0], area_size[1] * factor[1]) - area = im.resize(area_size, Image.BOX, area_box) + area = im.resize(area_size, Image.Resampling.BOX, area_box) reference.paste(area, (0, 0)) if area_size[0] < reduced.size[0]: assert reduced.size[0] - area_size[0] == 1 last_column_box = (area_box[2], 0, im.size[0], area_box[3]) - last_column = im.resize((1, area_size[1]), Image.BOX, last_column_box) + last_column = im.resize( + (1, area_size[1]), Image.Resampling.BOX, last_column_box + ) reference.paste(last_column, (area_size[0], 0)) if area_size[1] < reduced.size[1]: assert reduced.size[1] - area_size[1] == 1 last_row_box = (0, area_box[3], area_box[2], im.size[1]) - last_row = im.resize((area_size[0], 1), Image.BOX, last_row_box) + last_row = im.resize((area_size[0], 1), Image.Resampling.BOX, last_row_box) reference.paste(last_row, (0, area_size[1])) if area_size[0] < reduced.size[0] and area_size[1] < reduced.size[1]: last_pixel_box = (area_box[2], area_box[3], im.size[0], im.size[1]) - last_pixel = im.resize((1, 1), Image.BOX, last_pixel_box) + last_pixel = im.resize((1, 1), Image.Resampling.BOX, last_pixel_box) reference.paste(last_pixel, area_size) assert_compare_images(reduced, reference, average_diff, max_diff) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 8bf2ce916..125422337 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -24,7 +24,7 @@ class TestImagingResampleVulnerability: ): with pytest.raises(MemoryError): # any resampling filter will do here - im.im.resize((xsize, ysize), Image.BILINEAR) + im.im.resize((xsize, ysize), Image.Resampling.BILINEAR) def test_invalid_size(self): im = hopper() @@ -103,7 +103,7 @@ class TestImagingCoreResampleAccuracy: def test_reduce_box(self): for mode in ["RGBX", "RGB", "La", "L"]: case = self.make_case(mode, (8, 8), 0xE1) - case = case.resize((4, 4), Image.BOX) + case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off data = ("e1 e1" "e1 e1") @@ -114,7 +114,7 @@ class TestImagingCoreResampleAccuracy: def test_reduce_bilinear(self): for mode in ["RGBX", "RGB", "La", "L"]: case = self.make_case(mode, (8, 8), 0xE1) - case = case.resize((4, 4), Image.BILINEAR) + case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off data = ("e1 c9" "c9 b7") @@ -125,7 +125,7 @@ class TestImagingCoreResampleAccuracy: def test_reduce_hamming(self): for mode in ["RGBX", "RGB", "La", "L"]: case = self.make_case(mode, (8, 8), 0xE1) - case = case.resize((4, 4), Image.HAMMING) + case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off data = ("e1 da" "da d3") @@ -136,7 +136,7 @@ class TestImagingCoreResampleAccuracy: def test_reduce_bicubic(self): for mode in ["RGBX", "RGB", "La", "L"]: case = self.make_case(mode, (12, 12), 0xE1) - case = case.resize((6, 6), Image.BICUBIC) + case = case.resize((6, 6), Image.Resampling.BICUBIC) # fmt: off data = ("e1 e3 d4" "e3 e5 d6" @@ -148,7 +148,7 @@ class TestImagingCoreResampleAccuracy: def test_reduce_lanczos(self): for mode in ["RGBX", "RGB", "La", "L"]: case = self.make_case(mode, (16, 16), 0xE1) - case = case.resize((8, 8), Image.LANCZOS) + case = case.resize((8, 8), Image.Resampling.LANCZOS) # fmt: off data = ("e1 e0 e4 d7" "e0 df e3 d6" @@ -161,7 +161,7 @@ class TestImagingCoreResampleAccuracy: def test_enlarge_box(self): for mode in ["RGBX", "RGB", "La", "L"]: case = self.make_case(mode, (2, 2), 0xE1) - case = case.resize((4, 4), Image.BOX) + case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off data = ("e1 e1" "e1 e1") @@ -172,7 +172,7 @@ class TestImagingCoreResampleAccuracy: def test_enlarge_bilinear(self): for mode in ["RGBX", "RGB", "La", "L"]: case = self.make_case(mode, (2, 2), 0xE1) - case = case.resize((4, 4), Image.BILINEAR) + case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off data = ("e1 b0" "b0 98") @@ -183,7 +183,7 @@ class TestImagingCoreResampleAccuracy: def test_enlarge_hamming(self): for mode in ["RGBX", "RGB", "La", "L"]: case = self.make_case(mode, (2, 2), 0xE1) - case = case.resize((4, 4), Image.HAMMING) + case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off data = ("e1 d2" "d2 c5") @@ -194,7 +194,7 @@ class TestImagingCoreResampleAccuracy: def test_enlarge_bicubic(self): for mode in ["RGBX", "RGB", "La", "L"]: case = self.make_case(mode, (4, 4), 0xE1) - case = case.resize((8, 8), Image.BICUBIC) + case = case.resize((8, 8), Image.Resampling.BICUBIC) # fmt: off data = ("e1 e5 ee b9" "e5 e9 f3 bc" @@ -207,7 +207,7 @@ class TestImagingCoreResampleAccuracy: def test_enlarge_lanczos(self): for mode in ["RGBX", "RGB", "La", "L"]: case = self.make_case(mode, (6, 6), 0xE1) - case = case.resize((12, 12), Image.LANCZOS) + case = case.resize((12, 12), Image.Resampling.LANCZOS) data = ( "e1 e0 db ed f5 b8" "e0 df da ec f3 b7" @@ -220,7 +220,9 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (12, 12))) def test_box_filter_correct_range(self): - im = Image.new("RGB", (8, 8), "#1688ff").resize((100, 100), Image.BOX) + im = Image.new("RGB", (8, 8), "#1688ff").resize( + (100, 100), Image.Resampling.BOX + ) ref = Image.new("RGB", (100, 100), "#1688ff") assert_image_equal(im, ref) @@ -228,7 +230,7 @@ class TestImagingCoreResampleAccuracy: class TestCoreResampleConsistency: def make_case(self, mode, fill): im = Image.new(mode, (512, 9), fill) - return im.resize((9, 512), Image.LANCZOS), im.load()[0, 0] + return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] def run_case(self, case): channel, color = case @@ -283,20 +285,20 @@ class TestCoreResampleAlphaCorrect: @pytest.mark.xfail(reason="Current implementation isn't precise enough") def test_levels_rgba(self): case = self.make_levels_case("RGBA") - self.run_levels_case(case.resize((512, 32), Image.BOX)) - self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) - self.run_levels_case(case.resize((512, 32), Image.HAMMING)) - self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) - self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) @pytest.mark.xfail(reason="Current implementation isn't precise enough") def test_levels_la(self): case = self.make_levels_case("LA") - self.run_levels_case(case.resize((512, 32), Image.BOX)) - self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) - self.run_levels_case(case.resize((512, 32), Image.HAMMING)) - self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) - self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) def make_dirty_case(self, mode, clean_pixel, dirty_pixel): i = Image.new(mode, (64, 64), dirty_pixel) @@ -321,19 +323,27 @@ class TestCoreResampleAlphaCorrect: def test_dirty_pixels_rgba(self): case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BOX), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255, 255, 0)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255, 255, 0)) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.BILINEAR), (255, 255, 0) + ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.HAMMING), (255, 255, 0) + ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.BICUBIC), (255, 255, 0) + ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.LANCZOS), (255, 255, 0) + ) def test_dirty_pixels_la(self): case = self.make_dirty_case("LA", (255, 128), (0, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BOX), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BILINEAR), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.HAMMING), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BICUBIC), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.LANCZOS), (255,)) class TestCoreResamplePasses: @@ -346,26 +356,26 @@ class TestCoreResamplePasses: def test_horizontal(self): im = hopper("L") with self.count(1): - im.resize((im.size[0] - 10, im.size[1]), Image.BILINEAR) + im.resize((im.size[0] - 10, im.size[1]), Image.Resampling.BILINEAR) def test_vertical(self): im = hopper("L") with self.count(1): - im.resize((im.size[0], im.size[1] - 10), Image.BILINEAR) + im.resize((im.size[0], im.size[1] - 10), Image.Resampling.BILINEAR) def test_both(self): im = hopper("L") with self.count(2): - im.resize((im.size[0] - 10, im.size[1] - 10), Image.BILINEAR) + im.resize((im.size[0] - 10, im.size[1] - 10), Image.Resampling.BILINEAR) def test_box_horizontal(self): im = hopper("L") box = (20, 0, im.size[0] - 20, im.size[1]) with self.count(1): # the same size, but different box - with_box = im.resize(im.size, Image.BILINEAR, box) + with_box = im.resize(im.size, Image.Resampling.BILINEAR, box) with self.count(2): - cropped = im.crop(box).resize(im.size, Image.BILINEAR) + cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) assert_image_similar(with_box, cropped, 0.1) def test_box_vertical(self): @@ -373,9 +383,9 @@ class TestCoreResamplePasses: box = (0, 20, im.size[0], im.size[1] - 20) with self.count(1): # the same size, but different box - with_box = im.resize(im.size, Image.BILINEAR, box) + with_box = im.resize(im.size, Image.Resampling.BILINEAR, box) with self.count(2): - cropped = im.crop(box).resize(im.size, Image.BILINEAR) + cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) assert_image_similar(with_box, cropped, 0.1) @@ -388,7 +398,7 @@ class TestCoreResampleCoefficients: draw = ImageDraw.Draw(i) draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) - px = i.resize((5, i.size[1]), Image.BICUBIC).load() + px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load() if px[2, 0] != test_color // 2: assert test_color // 2 == px[2, 0] @@ -396,7 +406,7 @@ class TestCoreResampleCoefficients: # regression test for the wrong coefficients calculation # due to bug https://github.com/python-pillow/Pillow/issues/2161 im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) - histogram = im.resize((256, 256), Image.BICUBIC).histogram() + histogram = im.resize((256, 256), Image.Resampling.BICUBIC).histogram() # first channel assert histogram[0x100 * 0 + 0x20] == 0x10000 @@ -412,12 +422,12 @@ class TestCoreResampleBox: def test_wrong_arguments(self): im = hopper() for resample in ( - Image.NEAREST, - Image.BOX, - Image.BILINEAR, - Image.HAMMING, - Image.BICUBIC, - Image.LANCZOS, + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, ): im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height)) @@ -456,7 +466,7 @@ class TestCoreResampleBox: for y0, y1 in split_range(dst_size[1], ytiles): for x0, x1 in split_range(dst_size[0], xtiles): box = (x0 * scale[0], y0 * scale[1], x1 * scale[0], y1 * scale[1]) - tile = im.resize((x1 - x0, y1 - y0), Image.BICUBIC, box) + tile = im.resize((x1 - x0, y1 - y0), Image.Resampling.BICUBIC, box) tiled.paste(tile, (x0, y0)) return tiled @@ -467,7 +477,7 @@ class TestCoreResampleBox: with Image.open("Tests/images/flower.jpg") as im: assert im.size == (480, 360) dst_size = (251, 188) - reference = im.resize(dst_size, Image.BICUBIC) + reference = im.resize(dst_size, Image.Resampling.BICUBIC) for tiles in [(1, 1), (3, 3), (9, 7), (100, 100)]: tiled = self.resize_tiled(im, dst_size, *tiles) @@ -483,12 +493,16 @@ class TestCoreResampleBox: assert im.size == (480, 360) dst_size = (48, 36) # Reference is cropped image resized to destination - reference = im.crop((0, 0, 473, 353)).resize(dst_size, Image.BICUBIC) - # Image.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45) - supersampled = im.resize((60, 45), Image.BOX) + reference = im.crop((0, 0, 473, 353)).resize( + dst_size, Image.Resampling.BICUBIC + ) + # Image.Resampling.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45) + supersampled = im.resize((60, 45), Image.Resampling.BOX) - with_box = supersampled.resize(dst_size, Image.BICUBIC, (0, 0, 59.125, 44.125)) - without_box = supersampled.resize(dst_size, Image.BICUBIC) + with_box = supersampled.resize( + dst_size, Image.Resampling.BICUBIC, (0, 0, 59.125, 44.125) + ) + without_box = supersampled.resize(dst_size, Image.Resampling.BICUBIC) # error with box should be much smaller than without assert_image_similar(reference, with_box, 6) @@ -496,7 +510,7 @@ class TestCoreResampleBox: assert_image_similar(reference, without_box, 5) def test_formats(self): - for resample in [Image.NEAREST, Image.BILINEAR]: + for resample in [Image.Resampling.NEAREST, Image.Resampling.BILINEAR]: for mode in ["RGB", "L", "RGBA", "LA", "I", ""]: im = hopper(mode) box = (20, 20, im.size[0] - 20, im.size[1] - 20) @@ -514,7 +528,7 @@ class TestCoreResampleBox: ((40, 50), (10, 0, 50, 50)), ((40, 50), (10, 20, 50, 70)), ]: - res = im.resize(size, Image.LANCZOS, box) + res = im.resize(size, Image.Resampling.LANCZOS, box) assert res.size == size assert_image_equal(res, im.crop(box), f">>> {size} {box}") @@ -528,7 +542,7 @@ class TestCoreResampleBox: ((40, 50), (10.4, 0.4, 50.4, 50.4)), ((40, 50), (10.4, 20.4, 50.4, 70.4)), ]: - res = im.resize(size, Image.LANCZOS, box) + res = im.resize(size, Image.Resampling.LANCZOS, box) assert res.size == size with pytest.raises(AssertionError, match=r"difference \d"): # check that the difference at least that much @@ -538,7 +552,7 @@ class TestCoreResampleBox: # Can skip resize for one dimension im = hopper() - for flt in [Image.NEAREST, Image.BICUBIC]: + for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]: for size, box in [ ((40, 50), (0, 0, 40, 90)), ((40, 50), (0, 20, 40, 90)), @@ -559,7 +573,7 @@ class TestCoreResampleBox: # Can skip resize for one dimension im = hopper() - for flt in [Image.NEAREST, Image.BICUBIC]: + for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]: for size, box in [ ((40, 50), (0, 0, 90, 50)), ((40, 50), (20, 0, 90, 50)), diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 1fe278052..04b7c8c97 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -35,33 +35,33 @@ class TestImagingCoreResize: "I;16", ]: # exotic mode im = hopper(mode) - r = self.resize(im, (15, 12), Image.NEAREST) + r = self.resize(im, (15, 12), Image.Resampling.NEAREST) assert r.mode == mode assert r.size == (15, 12) assert r.im.bands == im.im.bands def test_convolution_modes(self): with pytest.raises(ValueError): - self.resize(hopper("1"), (15, 12), Image.BILINEAR) + self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR) with pytest.raises(ValueError): - self.resize(hopper("P"), (15, 12), Image.BILINEAR) + self.resize(hopper("P"), (15, 12), Image.Resampling.BILINEAR) with pytest.raises(ValueError): - self.resize(hopper("I;16"), (15, 12), Image.BILINEAR) + self.resize(hopper("I;16"), (15, 12), Image.Resampling.BILINEAR) for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]: im = hopper(mode) - r = self.resize(im, (15, 12), Image.BILINEAR) + r = self.resize(im, (15, 12), Image.Resampling.BILINEAR) assert r.mode == mode assert r.size == (15, 12) assert r.im.bands == im.im.bands def test_reduce_filters(self): for f in [ - Image.NEAREST, - Image.BOX, - Image.BILINEAR, - Image.HAMMING, - Image.BICUBIC, - Image.LANCZOS, + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, ]: r = self.resize(hopper("RGB"), (15, 12), f) assert r.mode == "RGB" @@ -69,12 +69,12 @@ class TestImagingCoreResize: def test_enlarge_filters(self): for f in [ - Image.NEAREST, - Image.BOX, - Image.BILINEAR, - Image.HAMMING, - Image.BICUBIC, - Image.LANCZOS, + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, ]: r = self.resize(hopper("RGB"), (212, 195), f) assert r.mode == "RGB" @@ -95,12 +95,12 @@ class TestImagingCoreResize: samples["dirty"].putpixel((1, 1), 128) for f in [ - Image.NEAREST, - Image.BOX, - Image.BILINEAR, - Image.HAMMING, - Image.BICUBIC, - Image.LANCZOS, + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, ]: # samples resized with current filter references = { @@ -124,12 +124,12 @@ class TestImagingCoreResize: def test_enlarge_zero(self): for f in [ - Image.NEAREST, - Image.BOX, - Image.BILINEAR, - Image.HAMMING, - Image.BICUBIC, - Image.LANCZOS, + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, ]: r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), f) assert r.mode == "RGB" @@ -164,15 +164,19 @@ def gradients_image(): class TestReducingGapResize: def test_reducing_gap_values(self, gradients_image): - ref = gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=None) - im = gradients_image.resize((52, 34), Image.BICUBIC) + ref = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, reducing_gap=None + ) + im = gradients_image.resize((52, 34), Image.Resampling.BICUBIC) assert_image_equal(ref, im) with pytest.raises(ValueError): - gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0) + gradients_image.resize((52, 34), Image.Resampling.BICUBIC, reducing_gap=0) with pytest.raises(ValueError): - gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0.99) + gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99 + ) def test_reducing_gap_1(self, gradients_image): for box, epsilon in [ @@ -180,9 +184,9 @@ class TestReducingGapResize: ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10), ]: - ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( - (52, 34), Image.BICUBIC, box=box, reducing_gap=1.0 + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 ) with pytest.raises(AssertionError): @@ -196,9 +200,9 @@ class TestReducingGapResize: ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1), ]: - ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( - (52, 34), Image.BICUBIC, box=box, reducing_gap=2.0 + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 ) with pytest.raises(AssertionError): @@ -212,9 +216,9 @@ class TestReducingGapResize: ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5), ]: - ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( - (52, 34), Image.BICUBIC, box=box, reducing_gap=3.0 + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 ) with pytest.raises(AssertionError): @@ -224,9 +228,9 @@ class TestReducingGapResize: def test_reducing_gap_8(self, gradients_image): for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]: - ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( - (52, 34), Image.BICUBIC, box=box, reducing_gap=8.0 + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 ) assert_image_equal(ref, im) @@ -236,8 +240,10 @@ class TestReducingGapResize: ((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5), ]: - ref = gradients_image.resize((52, 34), Image.BOX, box=box) - im = gradients_image.resize((52, 34), Image.BOX, box=box, reducing_gap=1.0) + ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) + im = gradients_image.resize( + (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 + ) assert_image_similar(ref, im, epsilon) @@ -261,12 +267,12 @@ class TestImageResize: def test_default_filter(self): for mode in "L", "RGB", "I", "F": im = hopper(mode) - assert im.resize((20, 20), Image.BICUBIC) == im.resize((20, 20)) + assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) for mode in "1", "P": im = hopper(mode) - assert im.resize((20, 20), Image.NEAREST) == im.resize((20, 20)) + assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) for mode in "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16": im = hopper(mode) - assert im.resize((20, 20), Image.NEAREST) == im.resize((20, 20)) + assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 2d72ffa68..f96864c53 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -46,14 +46,14 @@ def test_zero(): def test_resample(): # Target image creation, inspected by eye. # >>> im = Image.open('Tests/images/hopper.ppm') - # >>> im = im.rotate(45, resample=Image.BICUBIC, expand=True) + # >>> im = im.rotate(45, resample=Image.Resampling.BICUBIC, expand=True) # >>> im.save('Tests/images/hopper_45.png') with Image.open("Tests/images/hopper_45.png") as target: for (resample, epsilon) in ( - (Image.NEAREST, 10), - (Image.BILINEAR, 5), - (Image.BICUBIC, 0), + (Image.Resampling.NEAREST, 10), + (Image.Resampling.BILINEAR, 5), + (Image.Resampling.BICUBIC, 0), ): im = hopper() im = im.rotate(45, resample=resample, expand=True) @@ -62,7 +62,7 @@ def test_resample(): def test_center_0(): im = hopper() - im = im.rotate(45, center=(0, 0), resample=Image.BICUBIC) + im = im.rotate(45, center=(0, 0), resample=Image.Resampling.BICUBIC) with Image.open("Tests/images/hopper_45.png") as target: target_origin = target.size[1] / 2 @@ -73,7 +73,7 @@ def test_center_0(): def test_center_14(): im = hopper() - im = im.rotate(45, center=(14, 14), resample=Image.BICUBIC) + im = im.rotate(45, center=(14, 14), resample=Image.Resampling.BICUBIC) with Image.open("Tests/images/hopper_45.png") as target: target_origin = target.size[1] / 2 - 14 @@ -90,7 +90,7 @@ def test_translate(): (target_origin, target_origin, target_origin + 128, target_origin + 128) ) - im = im.rotate(45, translate=(5, 5), resample=Image.BICUBIC) + im = im.rotate(45, translate=(5, 5), resample=Image.Resampling.BICUBIC) assert_image_similar(im, target, 1) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index dd140955d..6d4eb4cd1 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -97,24 +97,24 @@ def test_DCT_scaling_edges(): thumb = fromstring(tostring(im, "JPEG", quality=99, subsampling=0)) # small reducing_gap to amplify the effect - thumb.thumbnail((32, 32), Image.BICUBIC, reducing_gap=1.0) + thumb.thumbnail((32, 32), Image.Resampling.BICUBIC, reducing_gap=1.0) - ref = im.resize((32, 32), Image.BICUBIC) + ref = im.resize((32, 32), Image.Resampling.BICUBIC) # This is still JPEG, some error is present. Without the fix it is 11.5 assert_image_similar(thumb, ref, 1.5) def test_reducing_gap_values(): im = hopper() - im.thumbnail((18, 18), Image.BICUBIC) + im.thumbnail((18, 18), Image.Resampling.BICUBIC) ref = hopper() - ref.thumbnail((18, 18), Image.BICUBIC, reducing_gap=2.0) + ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=2.0) # reducing_gap=2.0 should be the default assert_image_equal(ref, im) ref = hopper() - ref.thumbnail((18, 18), Image.BICUBIC, reducing_gap=None) + ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None) with pytest.raises(AssertionError): assert_image_equal(ref, im) @@ -125,9 +125,9 @@ def test_reducing_gap_for_DCT_scaling(): with Image.open("Tests/images/hopper.jpg") as ref: # thumbnail should call draft with reducing_gap scale ref.draft(None, (18 * 3, 18 * 3)) - ref = ref.resize((18, 18), Image.BICUBIC) + ref = ref.resize((18, 18), Image.Resampling.BICUBIC) with Image.open("Tests/images/hopper.jpg") as im: - im.thumbnail((18, 18), Image.BICUBIC, reducing_gap=3.0) + im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0) assert_image_equal(ref, im) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index ea208362b..ac0e74969 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -34,20 +34,22 @@ class TestImageTransform: def test_palette(self): with Image.open("Tests/images/hopper.gif") as im: - transformed = im.transform(im.size, Image.AFFINE, [1, 0, 0, 0, 1, 0]) + transformed = im.transform( + im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] + ) assert im.palette.palette == transformed.palette.palette def test_extent(self): im = hopper("RGB") (w, h) = im.size # fmt: off - transformed = im.transform(im.size, Image.EXTENT, + transformed = im.transform(im.size, Image.Transform.EXTENT, (0, 0, w//2, h//2), # ul -> lr - Image.BILINEAR) + Image.Resampling.BILINEAR) # fmt: on - scaled = im.resize((w * 2, h * 2), Image.BILINEAR).crop((0, 0, w, h)) + scaled = im.resize((w * 2, h * 2), Image.Resampling.BILINEAR).crop((0, 0, w, h)) # undone -- precision? assert_image_similar(transformed, scaled, 23) @@ -57,15 +59,18 @@ class TestImageTransform: im = hopper("RGB") (w, h) = im.size # fmt: off - transformed = im.transform(im.size, Image.QUAD, + transformed = im.transform(im.size, Image.Transform.QUAD, (0, 0, 0, h//2, # ul -> ccw around quad: w//2, h//2, w//2, 0), - Image.BILINEAR) + Image.Resampling.BILINEAR) # fmt: on scaled = im.transform( - (w, h), Image.AFFINE, (0.5, 0, 0, 0, 0.5, 0), Image.BILINEAR + (w, h), + Image.Transform.AFFINE, + (0.5, 0, 0, 0, 0.5, 0), + Image.Resampling.BILINEAR, ) assert_image_equal(transformed, scaled) @@ -80,9 +85,9 @@ class TestImageTransform: (w, h) = im.size transformed = im.transform( im.size, - Image.EXTENT, + Image.Transform.EXTENT, (0, 0, w * 2, h * 2), - Image.BILINEAR, + Image.Resampling.BILINEAR, fillcolor="red", ) @@ -93,18 +98,21 @@ class TestImageTransform: im = hopper("RGBA") (w, h) = im.size # fmt: off - transformed = im.transform(im.size, Image.MESH, + transformed = im.transform(im.size, Image.Transform.MESH, [((0, 0, w//2, h//2), # box (0, 0, 0, h, w, h, w, 0)), # ul -> ccw around quad ((w//2, h//2, w, h), # box (0, 0, 0, h, w, h, w, 0))], # ul -> ccw around quad - Image.BILINEAR) + Image.Resampling.BILINEAR) # fmt: on scaled = im.transform( - (w // 2, h // 2), Image.AFFINE, (2, 0, 0, 0, 2, 0), Image.BILINEAR + (w // 2, h // 2), + Image.Transform.AFFINE, + (2, 0, 0, 0, 2, 0), + Image.Resampling.BILINEAR, ) checker = Image.new("RGBA", im.size) @@ -137,14 +145,16 @@ class TestImageTransform: def test_alpha_premult_resize(self): def op(im, sz): - return im.resize(sz, Image.BILINEAR) + return im.resize(sz, Image.Resampling.BILINEAR) self._test_alpha_premult(op) def test_alpha_premult_transform(self): def op(im, sz): (w, h) = im.size - return im.transform(sz, Image.EXTENT, (0, 0, w, h), Image.BILINEAR) + return im.transform( + sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR + ) self._test_alpha_premult(op) @@ -171,7 +181,7 @@ class TestImageTransform: @pytest.mark.parametrize("mode", ("RGBA", "LA")) def test_nearest_resize(self, mode): def op(im, sz): - return im.resize(sz, Image.NEAREST) + return im.resize(sz, Image.Resampling.NEAREST) self._test_nearest(op, mode) @@ -179,7 +189,9 @@ class TestImageTransform: def test_nearest_transform(self, mode): def op(im, sz): (w, h) = im.size - return im.transform(sz, Image.EXTENT, (0, 0, w, h), Image.NEAREST) + return im.transform( + sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST + ) self._test_nearest(op, mode) @@ -213,13 +225,15 @@ class TestImageTransform: def test_unknown_resampling_filter(self): with hopper() as im: (w, h) = im.size - for resample in (Image.BOX, "unknown"): + for resample in (Image.Resampling.BOX, "unknown"): with pytest.raises(ValueError): - im.transform((100, 100), Image.EXTENT, (0, 0, w, h), resample) + im.transform( + (100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample + ) class TestImageTransformAffine: - transform = Image.AFFINE + transform = Image.Transform.AFFINE def _test_image(self): im = hopper("RGB") @@ -247,7 +261,11 @@ class TestImageTransformAffine: else: transposed = im - for resample in [Image.NEAREST, Image.BILINEAR, Image.BICUBIC]: + for resample in [ + Image.Resampling.NEAREST, + Image.Resampling.BILINEAR, + Image.Resampling.BICUBIC, + ]: transformed = im.transform( transposed.size, self.transform, matrix, resample ) @@ -257,13 +275,13 @@ class TestImageTransformAffine: self._test_rotate(0, None) def test_rotate_90_deg(self): - self._test_rotate(90, Image.ROTATE_90) + self._test_rotate(90, Image.Transpose.ROTATE_90) def test_rotate_180_deg(self): - self._test_rotate(180, Image.ROTATE_180) + self._test_rotate(180, Image.Transpose.ROTATE_180) def test_rotate_270_deg(self): - self._test_rotate(270, Image.ROTATE_270) + self._test_rotate(270, Image.Transpose.ROTATE_270) def _test_resize(self, scale, epsilonscale): im = self._test_image() @@ -273,9 +291,9 @@ class TestImageTransformAffine: matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0] for resample, epsilon in [ - (Image.NEAREST, 0), - (Image.BILINEAR, 2), - (Image.BICUBIC, 1), + (Image.Resampling.NEAREST, 0), + (Image.Resampling.BILINEAR, 2), + (Image.Resampling.BICUBIC, 1), ]: transformed = im.transform(size_up, self.transform, matrix_up, resample) transformed = transformed.transform( @@ -306,9 +324,9 @@ class TestImageTransformAffine: matrix_down = [1, 0, x, 0, 1, y, 0, 0] for resample, epsilon in [ - (Image.NEAREST, 0), - (Image.BILINEAR, 1.5), - (Image.BICUBIC, 1), + (Image.Resampling.NEAREST, 0), + (Image.Resampling.BILINEAR, 1.5), + (Image.Resampling.BICUBIC, 1), ]: transformed = im.transform(size_up, self.transform, matrix_up, resample) transformed = transformed.transform( @@ -328,4 +346,4 @@ class TestImageTransformAffine: class TestImageTransformPerspective(TestImageTransformAffine): # Repeat all tests for AFFINE transformations with PERSPECTIVE - transform = Image.PERSPECTIVE + transform = Image.Transform.PERSPECTIVE diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index a004434da..6408e1564 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,12 +1,4 @@ -from PIL.Image import ( - FLIP_LEFT_RIGHT, - FLIP_TOP_BOTTOM, - ROTATE_90, - ROTATE_180, - ROTATE_270, - TRANSPOSE, - TRANSVERSE, -) +from PIL.Image import Transpose from . import helper from .helper import assert_image_equal @@ -20,7 +12,7 @@ HOPPER = { def test_flip_left_right(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(FLIP_LEFT_RIGHT) + out = im.transpose(Transpose.FLIP_LEFT_RIGHT) assert out.mode == mode assert out.size == im.size @@ -37,7 +29,7 @@ def test_flip_left_right(): def test_flip_top_bottom(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(FLIP_TOP_BOTTOM) + out = im.transpose(Transpose.FLIP_TOP_BOTTOM) assert out.mode == mode assert out.size == im.size @@ -54,7 +46,7 @@ def test_flip_top_bottom(): def test_rotate_90(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(ROTATE_90) + out = im.transpose(Transpose.ROTATE_90) assert out.mode == mode assert out.size == im.size[::-1] @@ -71,7 +63,7 @@ def test_rotate_90(): def test_rotate_180(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(ROTATE_180) + out = im.transpose(Transpose.ROTATE_180) assert out.mode == mode assert out.size == im.size @@ -88,7 +80,7 @@ def test_rotate_180(): def test_rotate_270(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(ROTATE_270) + out = im.transpose(Transpose.ROTATE_270) assert out.mode == mode assert out.size == im.size[::-1] @@ -105,7 +97,7 @@ def test_rotate_270(): def test_transpose(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(TRANSPOSE) + out = im.transpose(Transpose.TRANSPOSE) assert out.mode == mode assert out.size == im.size[::-1] @@ -122,7 +114,7 @@ def test_transpose(): def test_tranverse(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(TRANSVERSE) + out = im.transpose(Transpose.TRANSVERSE) assert out.mode == mode assert out.size == im.size[::-1] @@ -143,20 +135,31 @@ def test_roundtrip(): def transpose(first, second): return im.transpose(first).transpose(second) - assert_image_equal(im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT)) - assert_image_equal(im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM)) - assert_image_equal(im, transpose(ROTATE_90, ROTATE_270)) - assert_image_equal(im, transpose(ROTATE_180, ROTATE_180)) assert_image_equal( - im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM) + im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT) ) assert_image_equal( - im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT) + im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM) + ) + assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270)) + assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180)) + assert_image_equal( + im.transpose(Transpose.TRANSPOSE), + transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM), ) assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT) + im.transpose(Transpose.TRANSPOSE), + transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT), ) assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM) + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM), + ) + assert_image_equal( + im.transpose(Transpose.TRANSVERSE), + transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE), ) - assert_image_equal(im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE)) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 99f3b4e03..e0093739c 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -140,7 +140,7 @@ def test_intent(): skip_missing() assert ImageCms.getDefaultIntent(SRGB) == 0 support = ImageCms.isIntentSupported( - SRGB, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT + SRGB, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT ) assert support == 1 @@ -153,7 +153,7 @@ def test_profile_object(): # ["sRGB built-in", "", "WhitePoint : D65 (daylight)", "", ""] assert ImageCms.getDefaultIntent(p) == 0 support = ImageCms.isIntentSupported( - p, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT + p, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT ) assert support == 1 @@ -593,3 +593,13 @@ def test_auxiliary_channels_isolated(): ) assert_image_equal(test_image.convert(dst_format[2]), reference_image) + + +def test_constants_deprecation(): + for enum, prefix in { + ImageCms.Intent: "INTENT_", + ImageCms.Direction: "DIRECTION_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(ImageCms, prefix + name) == enum[name] diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index b661494c7..3cd755cb4 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -183,7 +183,7 @@ def test_bitmap(): im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) with Image.open("Tests/images/pil123rgba.png") as small: - small = small.resize((50, 50), Image.NEAREST) + small = small.resize((50, 50), Image.Resampling.NEAREST) # Act draw.bitmap((10, 10), small) @@ -319,7 +319,7 @@ def test_ellipse_symmetric(): im = Image.new("RGB", (width, 100)) draw = ImageDraw.Draw(im) draw.ellipse(bbox, fill="green", outline="blue") - assert_image_equal(im, im.transpose(Image.FLIP_LEFT_RIGHT)) + assert_image_equal(im, im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)) def test_ellipse_width(): diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index a5c76700d..f3da73e38 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -23,7 +23,7 @@ class TestImageFile: def test_parser(self): def roundtrip(format): - im = hopper("L").resize((1000, 1000), Image.NEAREST) + im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") @@ -124,6 +124,23 @@ class TestImageFile: with pytest.raises(OSError): p.close() + def test_no_format(self): + buf = BytesIO(b"\x00" * 255) + + class DummyImageFile(ImageFile.ImageFile): + def _open(self): + self.mode = "RGB" + self._size = (1, 1) + + im = DummyImageFile(buf) + assert im.format is None + assert im.get_format_mimetype() is None + + def test_oserror(self): + im = Image.new("RGB", (1, 1)) + with pytest.raises(OSError): + im.save(BytesIO(), "JPEG2000", num_resolutions=2) + def test_truncated(self): b = BytesIO( b"BM000000000000" # head_data @@ -179,6 +196,11 @@ class MockPyDecoder(ImageFile.PyDecoder): return -1, 0 +class MockPyEncoder(ImageFile.PyEncoder): + def encode(self, buffer): + return 1, 1, b"" + + xoff, yoff, xsize, ysize = 10, 20, 100, 100 @@ -190,53 +212,58 @@ class MockImageFile(ImageFile.ImageFile): self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] -class TestPyDecoder: - def get_decoder(self): - decoder = MockPyDecoder(None) +class CodecsTest: + @classmethod + def setup_class(cls): + cls.decoder = MockPyDecoder(None) + cls.encoder = MockPyEncoder(None) - def closure(mode, *args): - decoder.__init__(mode, *args) - return decoder + def decoder_closure(mode, *args): + cls.decoder.__init__(mode, *args) + return cls.decoder - Image.register_decoder("MOCK", closure) - return decoder + def encoder_closure(mode, *args): + cls.encoder.__init__(mode, *args) + return cls.encoder + Image.register_decoder("MOCK", decoder_closure) + Image.register_encoder("MOCK", encoder_closure) + + +class TestPyDecoder(CodecsTest): def test_setimage(self): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - d = self.get_decoder() im.load() - assert d.state.xoff == xoff - assert d.state.yoff == yoff - assert d.state.xsize == xsize - assert d.state.ysize == ysize + assert self.decoder.state.xoff == xoff + assert self.decoder.state.yoff == yoff + assert self.decoder.state.xsize == xsize + assert self.decoder.state.ysize == ysize with pytest.raises(ValueError): - d.set_as_raw(b"\x00") + self.decoder.set_as_raw(b"\x00") def test_extents_none(self): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) im.tile = [("MOCK", None, 32, None)] - d = self.get_decoder() im.load() - assert d.state.xoff == 0 - assert d.state.yoff == 0 - assert d.state.xsize == 200 - assert d.state.ysize == 200 + assert self.decoder.state.xoff == 0 + assert self.decoder.state.yoff == 0 + assert self.decoder.state.xsize == 200 + assert self.decoder.state.ysize == 200 def test_negsize(self): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] - self.get_decoder() with pytest.raises(ValueError): im.load() @@ -250,7 +277,6 @@ class TestPyDecoder: im = MockImageFile(buf) im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] - self.get_decoder() with pytest.raises(ValueError): im.load() @@ -259,14 +285,90 @@ class TestPyDecoder: with pytest.raises(ValueError): im.load() - def test_no_format(self): + def test_decode(self): + decoder = ImageFile.PyDecoder(None) + with pytest.raises(NotImplementedError): + decoder.decode(None) + + +class TestPyEncoder(CodecsTest): + def test_setimage(self): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - assert im.format is None - assert im.get_format_mimetype() is None - def test_oserror(self): - im = Image.new("RGB", (1, 1)) - with pytest.raises(OSError): - im.save(BytesIO(), "JPEG2000", num_resolutions=2) + fp = BytesIO() + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] + ) + + assert self.encoder.state.xoff == xoff + assert self.encoder.state.yoff == yoff + assert self.encoder.state.xsize == xsize + assert self.encoder.state.ysize == ysize + + def test_extents_none(self): + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + im.tile = [("MOCK", None, 32, None)] + + fp = BytesIO() + ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) + + assert self.encoder.state.xoff == 0 + assert self.encoder.state.yoff == 0 + assert self.encoder.state.xsize == 200 + assert self.encoder.state.ysize == 200 + + def test_negsize(self): + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + with pytest.raises(ValueError): + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] + ) + + with pytest.raises(ValueError): + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] + ) + + def test_oversize(self): + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], + ) + + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], + ) + + def test_encode(self): + encoder = ImageFile.PyEncoder(None) + with pytest.raises(NotImplementedError): + encoder.encode(None) + + bytes_consumed, errcode = encoder.encode_to_pyfd() + assert bytes_consumed == 0 + assert ImageFile.ERRORS[errcode] == "bad configuration" + + encoder._pushes_fd = True + with pytest.raises(NotImplementedError): + encoder.encode_to_pyfd() + + with pytest.raises(NotImplementedError): + encoder.encode_to_file(None, None) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 0d423aab7..f9d0a4c4f 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -29,7 +29,7 @@ pytestmark = skip_unless_feature("freetype2") class TestImageFont: - LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC + LAYOUT_ENGINE = ImageFont.Layout.BASIC def get_font(self): return ImageFont.truetype( @@ -88,19 +88,6 @@ class TestImageFont: ImageFont.truetype(tempfile, FONT_SIZE) - def test_unavailable_layout_engine(self): - have_raqm = ImageFont.core.HAVE_RAQM - ImageFont.core.HAVE_RAQM = False - - try: - ttf = ImageFont.truetype( - FONT_PATH, FONT_SIZE, layout_engine=ImageFont.LAYOUT_RAQM - ) - finally: - ImageFont.core.HAVE_RAQM = have_raqm - - assert ttf.layout_engine == ImageFont.LAYOUT_BASIC - def _render(self, font): txt = "Hello World!" ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE) @@ -182,7 +169,7 @@ class TestImageFont: im = Image.new(mode, (1, 1), 0) d = ImageDraw.Draw(im) - if self.LAYOUT_ENGINE == ImageFont.LAYOUT_BASIC: + if self.LAYOUT_ENGINE == ImageFont.Layout.BASIC: length = d.textlength(text, f) assert length == length_basic else: @@ -294,7 +281,7 @@ class TestImageFont: word = "testing" font = self.get_font() - orientation = Image.ROTATE_90 + orientation = Image.Transpose.ROTATE_90 transposed_font = ImageFont.TransposedFont(font, orientation=orientation) # Original font @@ -333,7 +320,7 @@ class TestImageFont: # Arrange text = "mask this" font = self.get_font() - orientation = Image.ROTATE_90 + orientation = Image.Transpose.ROTATE_90 transposed_font = ImageFont.TransposedFont(font, orientation=orientation) # Act @@ -604,7 +591,7 @@ class TestImageFont: # Arrange t = self.get_font() # Act / Assert - if t.layout_engine == ImageFont.LAYOUT_BASIC: + if t.layout_engine == ImageFont.Layout.BASIC: with pytest.raises(KeyError): t.getmask("абвг", direction="rtl") with pytest.raises(KeyError): @@ -753,7 +740,7 @@ class TestImageFont: name, text = "quick", "Quick" path = f"Tests/images/test_anchor_{name}_{anchor}.png" - if self.LAYOUT_ENGINE == ImageFont.LAYOUT_RAQM: + if self.LAYOUT_ENGINE == ImageFont.Layout.RAQM: width, height = (129, 44) else: width, height = (128, 44) @@ -993,7 +980,7 @@ class TestImageFont: @skip_unless_feature("raqm") class TestImageFont_RaqmLayout(TestImageFont): - LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM + LAYOUT_ENGINE = ImageFont.Layout.RAQM def test_render_mono_size(): @@ -1004,7 +991,7 @@ def test_render_mono_size(): ttf = ImageFont.truetype( "Tests/fonts/DejaVuSans/DejaVuSans.ttf", 18, - layout_engine=ImageFont.LAYOUT_BASIC, + layout_engine=ImageFont.Layout.BASIC, ) draw.text((10, 10), "r" * 10, "black", ttf) @@ -1022,3 +1009,25 @@ def test_oom(test_file): font = ImageFont.truetype(BytesIO(f.read())) with pytest.raises(Image.DecompressionBombError): font.getmask("Test Text") + + +def test_raqm_missing_warning(monkeypatch): + monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) + with pytest.warns(UserWarning) as record: + font = ImageFont.truetype( + FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM + ) + assert font.layout_engine == ImageFont.Layout.BASIC + assert str(record[-1].message) == ( + "Raqm layout was requested, but Raqm is not available. " + "Falling back to basic layout." + ) + + +def test_constants_deprecation(): + for enum, prefix in { + ImageFont.Layout: "LAYOUT_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(ImageFont, prefix + name) == enum[name] diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 589cb5a21..930907939 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import ImageQt @@ -56,7 +58,5 @@ def test_image(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): ImageQt.ImageQt("Tests/images/hopper.gif") - - assert not record diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index bf19a6033..55d7c9479 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -51,6 +51,16 @@ def test_show(): assert ImageShow.show(im) +def test_show_without_viewers(): + viewers = ImageShow._viewers + ImageShow._viewers = [] + + im = hopper() + assert not ImageShow.show(im) + + ImageShow._viewers = viewers + + def test_viewer(): viewer = ImageShow.Viewer() diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 0571aabf4..688af7113 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -34,7 +34,7 @@ def test_basic(tmp_path): imOut = imIn.copy() verify(imOut) # copy - imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h)) + imOut = imIn.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) verify(imOut) # transform filename = str(tmp_path / "temp.im") diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 936474fe8..9735837bc 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import Image @@ -237,6 +239,5 @@ def test_no_resource_warning_for_numpy_array(): with Image.open(test_file) as im: # Act/Assert - with pytest.warns(None) as record: + with warnings.catch_warnings(): array(im) - assert not record diff --git a/docs/Makefile b/docs/Makefile index 686f0119e..0d352302f 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = sphinx-build +SPHINXBUILD = python3 -m sphinx.cmd.build PAPER = BUILDDIR = _build @@ -41,38 +41,48 @@ help: clean: -rm -rf $(BUILDDIR)/* +install-sphinx: + python3 -c "import sphinx" > /dev/null 2>&1 || python3 -m pip install sphinx + html: + $(MAKE) install-sphinx $(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: + $(MAKE) install-sphinx $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: + $(MAKE) install-sphinx $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: + $(MAKE) install-sphinx $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: + $(MAKE) install-sphinx $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: + $(MAKE) install-sphinx $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: + $(MAKE) install-sphinx $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ @@ -82,6 +92,7 @@ qthelp: @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc" devhelp: + $(MAKE) install-sphinx $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @@ -91,11 +102,13 @@ devhelp: @echo "# devhelp" epub: + $(MAKE) install-sphinx $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: + $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @@ -103,22 +116,26 @@ latex: "(use \`make latexpdf' here to do that automatically)." latexpdf: + $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: + $(MAKE) install-sphinx $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: + $(MAKE) install-sphinx $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: + $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @@ -126,28 +143,33 @@ texinfo: "(use \`make info' here to do that automatically)." info: + $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: + $(MAKE) install-sphinx $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: + $(MAKE) install-sphinx $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: + $(MAKE) install-sphinx $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: + $(MAKE) install-sphinx $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/deprecations.rst b/docs/deprecations.rst index a3abe81fa..0b82e4185 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -66,6 +66,82 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. ``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest ``viewer.show_file(path="test.jpg")`` instead. +Constants +~~~~~~~~~ + +.. deprecated:: 9.2.0 + +A number of constants have been deprecated and will be removed in Pillow 10.0.0 +(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.ORDERED`` ``Image.Dither.ORDERED`` +``Image.RASTERIZE`` ``Image.Dither.RASTERIZE`` +``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG`` +``Image.WEB`` ``Image.Palette.WEB`` +``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE`` +``Image.AFFINE`` ``Image.Transform.AFFINE`` +``Image.EXTENT`` ``Image.Transform.EXTENT`` +``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE`` +``Image.QUAD`` ``Image.Transform.QUAD`` +``Image.MESH`` ``Image.Transform.MESH`` +``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT`` +``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM`` +``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90`` +``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180`` +``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270`` +``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE`` +``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE`` +``Image.BOX`` ``Image.Resampling.BOX`` +``Image.BILINEAR`` ``Image.Resampling.BILNEAR`` +``Image.LINEAR`` ``Image.Resampling.BILNEAR`` +``Image.HAMMING`` ``Image.Resampling.HAMMING`` +``Image.BICUBIC`` ``Image.Resampling.BICUBIC`` +``Image.CUBIC`` ``Image.Resampling.BICUBIC`` +``Image.LANCZOS`` ``Image.Resampling.LANCZOS`` +``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS`` +``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT`` +``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE`` +``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE`` +``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + +FitsStubImagePlugin +~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.1.0 + +The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in +Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through +:mod:`~PIL.FitsImagePlugin` instead. + Removed features ---------------- diff --git a/docs/handbook/appendices.rst b/docs/handbook/appendices.rst index 6afaef071..347a8848b 100644 --- a/docs/handbook/appendices.rst +++ b/docs/handbook/appendices.rst @@ -8,4 +8,4 @@ Appendices image-file-formats text-anchors - writing-your-own-file-decoder + writing-your-own-image-plugin diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index bd44f63a3..17808dbc4 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -26,6 +26,20 @@ Fully supported formats .. contents:: +BLP +^^^ + +BLP is the Blizzard Mipmap Format, a texture format used in World of +Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1`` +images, and all types of ``BLP2`` images. + +Pillow supports writing BLP images. The :py:meth:`~PIL.Image.Image.save` method +can take the following keyword arguments: + +**blp_version** + If present and set to "BLP1", images will be saved as BLP1. Otherwise, images + will be saved as BLP2. + BMP ^^^ @@ -381,11 +395,12 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: * ``keep``: Only valid for JPEG files, will retain the original image setting. * ``4:4:4``, ``4:2:2``, ``4:2:0``: Specific sampling values - * ``-1``: equivalent to ``keep`` * ``0``: equivalent to ``4:4:4`` * ``1``: equivalent to ``4:2:2`` * ``2``: equivalent to ``4:2:0`` + If absent, the setting will be determined by libjpeg or libjpeg-turbo. + **qtables** If present, sets the qtables for the encoder. This is listed as an advanced option for wizards in the JPEG documentation. Use with @@ -696,12 +711,12 @@ parameter must be set to ``True``. The following parameters can also be set: operation to be used for this frame before rendering the next frame. Defaults to 0. - * 0 (:py:data:`~PIL.PngImagePlugin.APNG_DISPOSE_OP_NONE`, default) - + * 0 (:py:data:`~PIL.PngImagePlugin.Disposal.OP_NONE`, default) - No disposal is done on this frame before rendering the next frame. - * 1 (:py:data:`PIL.PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`) - + * 1 (:py:data:`PIL.PngImagePlugin.Disposal.OP_BACKGROUND`) - This frame's modified region is cleared to fully transparent black before rendering the next frame. - * 2 (:py:data:`~PIL.PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`) - + * 2 (:py:data:`~PIL.PngImagePlugin.Disposal.OP_PREVIOUS`) - This frame's modified region is reverted to the previous frame's contents before rendering the next frame. @@ -710,10 +725,10 @@ parameter must be set to ``True``. The following parameters can also be set: operation to be used for this frame before rendering the next frame. Defaults to 0. - * 0 (:py:data:`~PIL.PngImagePlugin.APNG_BLEND_OP_SOURCE`) - + * 0 (:py:data:`~PIL.PngImagePlugin.Blend.OP_SOURCE`) - All color components of this frame, including alpha, overwrite the previous output image contents. - * 1 (:py:data:`~PIL.PngImagePlugin.APNG_BLEND_OP_OVER`) - + * 1 (:py:data:`~PIL.PngImagePlugin.Blend.OP_OVER`) - This frame should be alpha composited with the previous output image contents. .. note:: @@ -1041,13 +1056,6 @@ Pillow reads and writes X bitmap files (mode ``1``). Read-only formats ----------------- -BLP -^^^ - -BLP is the Blizzard Mipmap Format, a texture format used in World of -Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1`` -images, and all types of ``BLP2`` images. - CUR ^^^ @@ -1064,6 +1072,13 @@ is commonly used in fax applications. The DCX decoder can read files containing When the file is opened, only the first image is read. You can use :py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images. +FITS +^^^^ + +.. versionadded:: 9.1.0 + +Pillow identifies and reads FITS files, commonly used for astronomy. + FLI, FLC ^^^^^^^^ @@ -1354,16 +1369,6 @@ Pillow provides a stub driver for BUFR files. To add read or write support to your application, use :py:func:`PIL.BufrStubImagePlugin.register_handler`. -FITS -^^^^ - -.. versionadded:: 1.1.5 - -Pillow provides a stub driver for FITS files. - -To add read or write support to your application, use -:py:func:`PIL.FitsStubImagePlugin.register_handler`. - GRIB ^^^^ diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index aa9efe192..b0dbffda4 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -155,7 +155,7 @@ Processing a subrectangle, and pasting it back :: - region = region.transpose(Image.ROTATE_180) + region = region.transpose(Image.Transpose.ROTATE_180) im.paste(region, box) When pasting regions back, the size of the region must match the given region @@ -238,11 +238,11 @@ Transposing an image :: - out = im.transpose(Image.FLIP_LEFT_RIGHT) - out = im.transpose(Image.FLIP_TOP_BOTTOM) - out = im.transpose(Image.ROTATE_90) - out = im.transpose(Image.ROTATE_180) - out = im.transpose(Image.ROTATE_270) + out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + out = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + out = im.transpose(Image.Transpose.ROTATE_90) + out = im.transpose(Image.Transpose.ROTATE_180) + out = im.transpose(Image.Transpose.ROTATE_270) ``transpose(ROTATE)`` operations can also be performed identically with :py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-image-plugin.rst similarity index 93% rename from docs/handbook/writing-your-own-file-decoder.rst rename to docs/handbook/writing-your-own-image-plugin.rst index f69da9a94..0c9cfe8e8 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -4,10 +4,9 @@ Writing Your Own Image Plugin ============================= Pillow uses a plugin model which allows you to add your own -decoders to the library, without any changes to the library -itself. Such plugins usually have names like -:file:`XxxImagePlugin.py`, where ``Xxx`` is a unique format name -(usually an abbreviation). +decoders and encoders to the library, without any changes to the library +itself. Such plugins usually have names like :file:`XxxImagePlugin.py`, +where ``Xxx`` is a unique format name (usually an abbreviation). .. warning:: Pillow >= 2.1.0 no longer automatically imports any file in the Python path with a name ending in @@ -413,23 +412,24 @@ value, or if there is a read error from the file. This function should free any allocated memory and release any resources from external libraries. -.. _file-decoders-py: +.. _file-codecs-py: -Writing Your Own File Decoder in Python -======================================= +Writing Your Own File Codec in Python +===================================== -Python file decoders should derive from -:py:class:`PIL.ImageFile.PyDecoder` and should at least override the -decode method. File decoders should be registered using -:py:meth:`PIL.Image.register_decoder`. As in the C implementation of -the file decoders, there are three stages in the lifetime of a -Python-based file decoder: +Python file decoders and encoders should derive from +:py:class:`PIL.ImageFile.PyDecoder` and :py:class:`PIL.ImageFile.PyEncoder` +respectively, and should at least override the decode or encode method. +They should be registered using :py:meth:`PIL.Image.register_decoder` and +:py:meth:`PIL.Image.register_encoder`. As in the C implementation of +the file codecs, there are three stages in the lifetime of a +Python-based file codec: 1. Setup: Pillow looks for the decoder in the registry, then instantiates the class. -2. Decoding: The decoder instance's ``decode`` method is repeatedly - called with a buffer of data to be interpreted. - -3. Cleanup: The decoder instance's ``cleanup`` method is called. +2. Transforming: The instance's ``decode`` method is repeatedly called with + a buffer of data to be interpreted, or the ``encode`` method is repeatedly + called with the size of data to be output. +3. Cleanup: The instance's ``cleanup`` method is called. diff --git a/docs/installation.rst b/docs/installation.rst index 030916ef2..3e0446a5d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -169,7 +169,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.13**. + above uses liblcms2. Tested with **1.19** and **2.7-2.13.1**. * **libwebp** provides the WebP format. @@ -215,7 +215,7 @@ Many of Pillow's features require external libraries: Once you have installed the prerequisites, run:: python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade Pillow --no-binary :all: If the prerequisites are installed in the standard library locations for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no @@ -225,7 +225,7 @@ those locations by editing :file:`setup.py` or :file:`setup.cfg`, or by adding environment variables on the command line:: - CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow + CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: If Pillow has been previously built without the required prerequisites, it may be necessary to manually clear the pip cache or @@ -291,7 +291,7 @@ tools. The easiest way to install external libraries is via `Homebrew `_. After you install Homebrew, run:: - brew install libtiff libjpeg webp little-cms2 + brew install libjpeg libtiff little-cms2 openjpeg webp To install libraqm on macOS use Homebrew to install its dependencies:: @@ -302,7 +302,7 @@ Then see ``depends/install_raqm_cmake.sh`` to install libraqm. Now install Pillow with:: python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade Pillow --no-binary :all: or from within the uncompressed source directory:: @@ -349,7 +349,7 @@ Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: Now install Pillow with:: python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade Pillow --no-binary :all: Building on FreeBSD @@ -455,6 +455,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | CentOS Stream 8 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| CentOS Stream 9 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Debian 10 Buster | 3.7 | x86 | +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | @@ -463,6 +465,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 35 | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Gentoo | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | macOS 10.15 Catalina | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | @@ -496,11 +500,11 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.0.0 |arm | +| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.0.1 |arm | +----------------------------------+---------------------------+------------------+--------------+ | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | | +---------------------------+------------------+--------------+ -| | 3.7, 3.8, 3.9, 3.10 | 9.0.0 |x86-64 | +| | 3.7, 3.8, 3.9, 3.10 | 9.0.1 |x86-64 | | +---------------------------+------------------+--------------+ | | 3.6 | 8.4.0 |x86-64 | +----------------------------------+---------------------------+------------------+--------------+ diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index c80b28a98..2613b6585 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -254,7 +254,8 @@ This rotates the input image by ``theta`` degrees counter clockwise: .. automethod:: PIL.Image.Image.transform .. automethod:: PIL.Image.Image.transpose -This flips the input image by using the :data:`FLIP_LEFT_RIGHT` method. +This flips the input image by using the :data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT` +method. .. code-block:: python @@ -263,9 +264,9 @@ This flips the input image by using the :data:`FLIP_LEFT_RIGHT` method. with Image.open("hopper.jpg") as im: # Flip the image from left to right - im_flipped = im.transpose(method=Image.FLIP_LEFT_RIGHT) + im_flipped = im.transpose(method=Image.Transpose.FLIP_LEFT_RIGHT) # To flip the image from top to bottom, - # use the method "Image.FLIP_TOP_BOTTOM" + # use the method "Image.Transpose.FLIP_TOP_BOTTOM" .. automethod:: PIL.Image.Image.verify @@ -389,68 +390,57 @@ Transpose methods Used to specify the :meth:`Image.transpose` method to use. -.. data:: FLIP_LEFT_RIGHT -.. data:: FLIP_TOP_BOTTOM -.. data:: ROTATE_90 -.. data:: ROTATE_180 -.. data:: ROTATE_270 -.. data:: TRANSPOSE -.. data:: TRANSVERSE +.. autoclass:: Transpose + :members: + :undoc-members: Transform methods ^^^^^^^^^^^^^^^^^ Used to specify the :meth:`Image.transform` method to use. -.. data:: AFFINE +.. py:class:: Transform - Affine transform + .. py:attribute:: AFFINE -.. data:: EXTENT + Affine transform - Cut out a rectangular subregion + .. py:attribute:: EXTENT -.. data:: PERSPECTIVE + Cut out a rectangular subregion - Perspective transform + .. py:attribute:: PERSPECTIVE -.. data:: QUAD + Perspective transform - Map a quadrilateral to a rectangle + .. py:attribute:: QUAD -.. data:: MESH + Map a quadrilateral to a rectangle - Map a number of source quadrilaterals in one operation + .. py:attribute:: MESH + + Map a number of source quadrilaterals in one operation Resampling filters ^^^^^^^^^^^^^^^^^^ See :ref:`concept-filters` for details. -.. data:: NEAREST - :noindex: -.. data:: BOX - :noindex: -.. data:: BILINEAR - :noindex: -.. data:: HAMMING - :noindex: -.. data:: BICUBIC - :noindex: -.. data:: LANCZOS - :noindex: +.. autoclass:: Resampling + :members: + :undoc-members: -Some filters are also available under the following names for backwards compatibility: +Some deprecated filters are also available under the following names: .. data:: NONE :noindex: - :value: NEAREST + :value: Resampling.NEAREST .. data:: LINEAR - :value: BILINEAR + :value: Resampling.BILINEAR .. data:: CUBIC - :value: BICUBIC + :value: Resampling.BICUBIC .. data:: ANTIALIAS - :value: LANCZOS + :value: Resampling.LANCZOS Dither modes ^^^^^^^^^^^^ @@ -458,48 +448,56 @@ Dither modes Used to specify the dithering method to use for the :meth:`~Image.convert` and :meth:`~Image.quantize` methods. -.. data:: NONE - :noindex: +.. py:class:: Dither - No dither + .. py:attribute:: NONE -.. comment: (not implemented) - .. data:: ORDERED - .. data:: RASTERIZE + No dither -.. data:: FLOYDSTEINBERG + .. py:attribute:: ORDERED - Floyd-Steinberg dither + Not implemented + + .. py:attribute:: RASTERIZE + + Not implemented + + .. py:attribute:: FLOYDSTEINBERG + + Floyd-Steinberg dither Palettes ^^^^^^^^ Used to specify the pallete to use for the :meth:`~Image.convert` method. -.. data:: WEB -.. data:: ADAPTIVE +.. autoclass:: Palette + :members: + :undoc-members: Quantization methods ^^^^^^^^^^^^^^^^^^^^ Used to specify the quantization method to use for the :meth:`~Image.quantize` method. -.. data:: MEDIANCUT +.. py:class:: Quantize - Median cut. Default method, except for RGBA images. This method does not support - RGBA images. + .. py:attribute:: MEDIANCUT -.. data:: MAXCOVERAGE + Median cut. Default method, except for RGBA images. This method does not support + RGBA images. - Maximum coverage. This method does not support RGBA images. + .. py:attribute:: MAXCOVERAGE -.. data:: FASTOCTREE + Maximum coverage. This method does not support RGBA images. - Fast octree. Default method for RGBA images. + .. py:attribute:: FASTOCTREE -.. data:: LIBIMAGEQUANT + Fast octree. Default method for RGBA images. - libimagequant + .. py:attribute:: LIBIMAGEQUANT - Check support using :py:func:`PIL.features.check_feature` - with ``feature="libimagequant"``. + libimagequant + + Check support using :py:func:`PIL.features.check_feature` with + ``feature="libimagequant"``. diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index f938e63a0..9b9b5e7b2 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -118,8 +118,8 @@ can be easily displayed in a chromaticity diagram, for example). another profile (usually overridden at run-time, but provided here for DeviceLink and embedded source profiles, see 7.2.15 of ICC.1:2010). - One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, ``ImageCms.INTENT_PERCEPTUAL``, - ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and ``ImageCms.INTENT_SATURATION``. + One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and ``ImageCms.Intent.SATURATION``. .. py:attribute:: profile_id :type: bytes @@ -313,14 +313,14 @@ can be easily displayed in a chromaticity diagram, for example). the CLUT model. The dictionary is indexed by intents - (``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, - ``ImageCms.INTENT_PERCEPTUAL``, - ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and - ``ImageCms.INTENT_SATURATION``). + (``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, + ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and + ``ImageCms.Intent.SATURATION``). The values are 3-tuples indexed by directions - (``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``, - ``ImageCms.DIRECTION_PROOF``). + (``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``, + ``ImageCms.Direction.PROOF``). The elements of the tuple are booleans. If the value is ``True``, that intent is supported for that direction. @@ -331,14 +331,14 @@ can be easily displayed in a chromaticity diagram, for example). Returns a dictionary of all supported intents and directions. The dictionary is indexed by intents - (``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, - ``ImageCms.INTENT_PERCEPTUAL``, - ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and - ``ImageCms.INTENT_SATURATION``). + (``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, + ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and + ``ImageCms.Intent.SATURATION``). The values are 3-tuples indexed by directions - (``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``, - ``ImageCms.DIRECTION_PROOF``). + (``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``, + ``ImageCms.Direction.PROOF``). The elements of the tuple are booleans. If the value is ``True``, that intent is supported for that direction. @@ -352,11 +352,11 @@ can be easily displayed in a chromaticity diagram, for example). Note that you can also get this information for all intents and directions with :py:attr:`.intent_supported`. - :param intent: One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, - ``ImageCms.INTENT_PERCEPTUAL``, - ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` - and ``ImageCms.INTENT_SATURATION``. - :param direction: One of ``ImageCms.DIRECTION_INPUT``, - ``ImageCms.DIRECTION_OUTPUT`` - and ``ImageCms.DIRECTION_PROOF`` + :param intent: One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, + ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` + and ``ImageCms.Intent.SATURATION``. + :param direction: One of ``ImageCms.Direction.INPUT``, + ``ImageCms.Direction.OUTPUT`` + and ``ImageCms.Direction.PROOF`` :return: Boolean if the intent and direction is supported. diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index e0ce389e8..3cf59c610 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -40,8 +40,16 @@ Classes .. autoclass:: PIL.ImageFile.Parser() :members: +.. autoclass:: PIL.ImageFile.PyCodec() + :members: + .. autoclass:: PIL.ImageFile.PyDecoder() :members: + :show-inheritance: + +.. autoclass:: PIL.ImageFile.PyEncoder() + :members: + :show-inheritance: .. autoclass:: PIL.ImageFile.ImageFile() :member-order: bysource diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 5f718ce19..8efef7cfd 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -60,12 +60,12 @@ Methods Constants --------- -.. data:: PIL.ImageFont.LAYOUT_BASIC +.. data:: PIL.ImageFont.Layout.BASIC Use basic text layout for TrueType font. Advanced features such as text direction are not supported. -.. data:: PIL.ImageFont.LAYOUT_RAQM +.. data:: PIL.ImageFont.Layout.RAQM Use Raqm text layout for TrueType font. Advanced features are supported. diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst index 45b50c846..5cedede69 100644 --- a/docs/reference/ImageShow.rst +++ b/docs/reference/ImageShow.rst @@ -23,6 +23,9 @@ All default viewers convert the image to be shown to PNG format. .. autoclass:: PIL.ImageShow.EogViewer .. autoclass:: PIL.ImageShow.XVViewer + To provide maximum functionality on Unix-based systems, temporary files created + from images will not be automatically removed by Pillow. + .. autofunction:: PIL.ImageShow.register .. autoclass:: PIL.ImageShow.Viewer :member-order: bysource diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index 2ba95b8a6..dc4c2bf94 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -53,7 +53,7 @@ Then ``sudo apt-get update && sudo apt-get install libtiff5-dbgsym`` virtualenv -p python3.8-dbg ~/vpy38-dbg source ~/vpy38-dbg/bin/activate - cd ~/Pillow && pip install -r requirements.txt && make install + cd ~/Pillow && make install Test Case --------- diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 0a6381098..c66193061 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -57,7 +57,7 @@ Support for the following features can be checked: * ``transp_webp``: Support for transparency in WebP images. * ``webp_mux``: (compile time) Support for EXIF data in WebP images. * ``webp_anim``: (compile time) Support for animated WebP images. -* ``raqm``: Raqm library, required for ``ImageFont.LAYOUT_RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. +* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 7094f8784..fcf4514a8 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -41,10 +41,10 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.FitsStubImagePlugin` Module +:mod:`~PIL.FitsImagePlugin` Module -------------------------------------- -.. automodule:: PIL.FitsStubImagePlugin +.. automodule:: PIL.FitsImagePlugin :members: :undoc-members: :show-inheritance: @@ -230,8 +230,7 @@ Plugin reference .. automodule:: PIL.PngImagePlugin :members: ChunkStream, PngImageFile, PngStream, getchunks, is_cid, putchunk, - MAX_TEXT_CHUNK, MAX_TEXT_MEMORY, APNG_BLEND_OP_SOURCE, APNG_BLEND_OP_OVER, - APNG_DISPOSE_OP_NONE, APNG_DISPOSE_OP_BACKGROUND, APNG_DISPOSE_OP_PREVIOUS + Blend, Disposal, MAX_TEXT_CHUNK, MAX_TEXT_MEMORY :undoc-members: :show-inheritance: :member-order: groupwise diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index 660d33164..dda814c1f 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -111,16 +111,14 @@ downscaling with libjpeg, which uses supersampling internally, not convolutions. Image transposition ------------------- -A new method :py:data:`PIL.Image.TRANSPOSE` has been added for the +A new method ``TRANSPOSE`` has been added for the :py:meth:`~PIL.Image.Image.transpose` operation in addition to -:py:data:`~PIL.Image.FLIP_LEFT_RIGHT`, :py:data:`~PIL.Image.FLIP_TOP_BOTTOM`, -:py:data:`~PIL.Image.ROTATE_90`, :py:data:`~PIL.Image.ROTATE_180`, -:py:data:`~PIL.Image.ROTATE_270`. :py:data:`~PIL.Image.TRANSPOSE` is an algebra -transpose, with an image reflected across its main diagonal. +``FLIP_LEFT_RIGHT``, ``FLIP_TOP_BOTTOM``, ``ROTATE_90``, ``ROTATE_180``, +``ROTATE_270``. ``TRANSPOSE`` is an algebra transpose, with an image reflected +across its main diagonal. -The speed of :py:data:`~PIL.Image.ROTATE_90`, :py:data:`~PIL.Image.ROTATE_270` -and :py:data:`~PIL.Image.TRANSPOSE` has been significantly improved for large -images which don't fit in the processor cache. +The speed of ``ROTATE_90``, ``ROTATE_270`` and ``TRANSPOSE`` has been significantly +improved for large images which don't fit in the processor cache. Gaussian blur and unsharp mask ------------------------------ diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst new file mode 100644 index 000000000..22b185e95 --- /dev/null +++ b/docs/releasenotes/9.1.0.rst @@ -0,0 +1,185 @@ +9.1.0 +----- + +API Changes +=========== + +Raise an error when performing a negative crop +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Performing a negative crop on an image previously just returned a ``(0, 0)`` image. Now +it will raise a ``ValueError``, to help reduce confusion if a user has unintentionally +provided the wrong arguments. + +Added specific error if path coordinate type is incorrect +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rather than returning a ``SystemError``, passing the incorrect types of coordinates into +a path will now raise a more specific ``ValueError``, with the message "incorrect +coordinate type". + +Replace requirements.txt with extras +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rather than installing all dependencies for docs and tests via ``requirements.txt``, +``extras_require`` is used instead. This installs only those needed and at the same +time as installing Pillow. + +For example: + +.. code-block:: bash + + # Install with dependencies for tests: + python3 -m pip install .[tests] + + # Or for building docs: + python3 -m pip install .[docs] + + # Or for all: + python3 -m pip install .[docs,tests] + +On macOS, the last argument may need to be wrapped in quotes, e.g. +``python3 -m pip install ".[tests]"`` + +Therefore ``requirements.txt`` has been removed along with the ``make install-req`` +command for installing its contents. + +Deprecations +============ + +Constants +^^^^^^^^^ + +A number of constants have been deprecated and will be removed in Pillow 10.0.0 +(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.ORDERED`` ``Image.Dither.ORDERED`` +``Image.RASTERIZE`` ``Image.Dither.RASTERIZE`` +``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG`` +``Image.WEB`` ``Image.Palette.WEB`` +``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE`` +``Image.AFFINE`` ``Image.Transform.AFFINE`` +``Image.EXTENT`` ``Image.Transform.EXTENT`` +``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE`` +``Image.QUAD`` ``Image.Transform.QUAD`` +``Image.MESH`` ``Image.Transform.MESH`` +``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT`` +``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM`` +``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90`` +``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180`` +``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270`` +``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE`` +``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE`` +``Image.BOX`` ``Image.Resampling.BOX`` +``Image.BILINEAR`` ``Image.Resampling.BILNEAR`` +``Image.LINEAR`` ``Image.Resampling.BILNEAR`` +``Image.HAMMING`` ``Image.Resampling.HAMMING`` +``Image.BICUBIC`` ``Image.Resampling.BICUBIC`` +``Image.CUBIC`` ``Image.Resampling.BICUBIC`` +``Image.LANCZOS`` ``Image.Resampling.LANCZOS`` +``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS`` +``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT`` +``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE`` +``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE`` +``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + +ImageShow.Viewer.show_file file argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by +``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. +``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest +``viewer.show_file(path="test.jpg")`` instead. + +FitsStubImagePlugin +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.1.0 + +The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in +Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through +:mod:`~PIL.FitsImagePlugin` instead. + +API Additions +============= + +Added get_photoshop_blocks() to parse Photoshop TIFF tag +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.TiffImagePlugin.TiffImageFile.get_photoshop_blocks` has been added, to +allow users to determine what Photoshop "Image Resource Blocks" are contained within an +image. The keys of the returned dictionary are the image resource IDs. + +At present, the information within each block is merely returned as a dictionary with a +"data" entry. This will allow more useful information to be added in the future without +breaking backwards compatibility. + +Added rawmode argument to Image.getpalette() +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +By default, :py:meth:`~PIL.Image.Image.getpalette` returns RGB data from the palette. +A ``rawmode`` argument has been added, to allow the mode to be chosen instead. ``None`` +can be used to return data in the current mode of the palette. + +Added PyEncoder +^^^^^^^^^^^^^^^ + +:py:class:`~PIL.ImageFile.PyEncoder` has been added, allowing for file encoders to be +written in Python. See :ref:`Writing Your Own File Codec in Python` for +more information. + +Other Changes +============= + +ImageShow temporary files on Unix +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When calling :py:meth:`~PIL.Image.Image.show` or using :py:mod:`~PIL.ImageShow`, +a temporary file is created from the image. On Unix, Pillow will no longer delete these +files, and instead leave it to the operating system to do so. + +Image._repr_pretty_ +^^^^^^^^^^^^^^^^^^^ + +``im._repr_pretty_`` has been added to provide a representation of an image without the +identity of the object. This allows Jupyter to describe an image and have that +description stay the same on subsequent executions of the same code. + +Added BLP saving +^^^^^^^^^^^^^^^^ + +Support has been added for saving BLP images. ``blp_version`` can be used to specify +whether the image should be saved as BLP1 or BLP2, e.g. +``im.save("out.blp", blp_version="BLP1")``. By default, BLP2 will be used. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index e9b11c220..656acef95 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 9.1.0 9.0.1 9.0.0 8.4.0 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 1e150e304..000000000 --- a/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -# Development, documentation & testing requirements. -black -check-manifest -coverage -defusedxml -markdown2 -olefile -packaging -pyroma -pytest -pytest-cov -pytest-timeout -sphinx>=2.4 -sphinx-copybutton -sphinx-issues>=3.0.1 -sphinx-removed-in -sphinx-rtd-theme>=1.0 -sphinxext-opengraph diff --git a/selftest.py b/selftest.py index 4ebd7cc00..5010c1213 100755 --- a/selftest.py +++ b/selftest.py @@ -97,9 +97,9 @@ def testimage(): 10456 >>> len(im.tobytes()) 49152 - >>> _info(im.transform((512, 512), Image.AFFINE, (1,0,0,0,1,0))) + >>> _info(im.transform((512, 512), Image.Transform.AFFINE, (1,0,0,0,1,0))) (None, 'RGB', (512, 512)) - >>> _info(im.transform((512, 512), Image.EXTENT, (32,32,96,96))) + >>> _info(im.transform((512, 512), Image.Transform.EXTENT, (32,32,96,96))) (None, 'RGB', (512, 512)) The ImageDraw module lets you draw stuff in raster images: diff --git a/setup.cfg b/setup.cfg index c3b5a3197..5ca5831cf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,27 @@ project_urls = [options] python_requires = >=3.7 +[options.extras_require] +docs = + olefile + sphinx>=2.4 + sphinx-copybutton + sphinx-issues>=3.0.1 + sphinx-removed-in + sphinx-rtd-theme>=1.0 + sphinxext-opengraph +tests = + check-manifest + coverage + defusedxml + markdown2 + olefile + packaging + pyroma + pytest + pytest-cov + pytest-timeout + [flake8] extend-ignore = E203 max-line-length = 88 diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 7b78597b4..779fddea8 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -29,20 +29,56 @@ BLP files come in many different flavours: - DXT5 compression is used if alpha_encoding == 7. """ +import os import struct +import warnings +from enum import IntEnum from io import BytesIO from . import Image, ImageFile -BLP_FORMAT_JPEG = 0 -BLP_ENCODING_UNCOMPRESSED = 1 -BLP_ENCODING_DXT = 2 -BLP_ENCODING_UNCOMPRESSED_RAW_BGRA = 3 +class Format(IntEnum): + JPEG = 0 -BLP_ALPHA_ENCODING_DXT1 = 0 -BLP_ALPHA_ENCODING_DXT3 = 1 -BLP_ALPHA_ENCODING_DXT5 = 7 + +class Encoding(IntEnum): + UNCOMPRESSED = 1 + DXT = 2 + UNCOMPRESSED_RAW_BGRA = 3 + + +class AlphaEncoding(IntEnum): + DXT1 = 0 + DXT3 = 1 + DXT5 = 7 + + +def __getattr__(name): + deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). " + for enum, prefix in { + Format: "BLP_FORMAT_", + Encoding: "BLP_ENCODING_", + AlphaEncoding: "BLP_ALPHA_ENCODING_", + }.items(): + if name.startswith(prefix): + name = name[len(prefix) :] + if name in enum.__members__: + warnings.warn( + prefix + + name + + " is " + + deprecated + + "Use " + + enum.__name__ + + "." + + name + + " instead.", + DeprecationWarning, + stacklevel=2, + ) + return enum[name] + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") def unpack_565(i): @@ -231,6 +267,10 @@ class BLPFormatError(NotImplementedError): pass +def _accept(prefix): + return prefix[:4] in (b"BLP1", b"BLP2") + + class BlpImageFile(ImageFile.ImageFile): """ Blizzard Mipmap Format @@ -241,51 +281,52 @@ class BlpImageFile(ImageFile.ImageFile): def _open(self): self.magic = self.fp.read(4) - self._read_blp_header() - if self.magic == b"BLP1": - decoder = "BLP1" - self.mode = "RGB" - elif self.magic == b"BLP2": - decoder = "BLP2" - self.mode = "RGBA" if self._blp_alpha_depth else "RGB" + self.fp.seek(5, os.SEEK_CUR) + (self._blp_alpha_depth,) = struct.unpack("u2", None), - "I;16L": ("i2", None), - "I;16LS": ("u4", None), - "I;32L": ("i4", None), - "I;32LS": ("" def _conv_type_shape(im): - typ, extra = _MODE_CONV[im.mode] - if extra is None: - return (im.size[1], im.size[0]), typ - else: - return (im.size[1], im.size[0], extra), typ + m = ImageMode.getmode(im.mode) + shape = (im.height, im.width) + extra = len(m.bands) + if extra != 1: + shape += (extra,) + return shape, m.typestr MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"] @@ -821,13 +850,7 @@ class Image: if self.im and self.palette and self.palette.dirty: # realize palette mode, arr = self.palette.getdata() - if mode == "RGBA": - mode = "RGB" - self.info["transparency"] = arr[3::4] - arr = bytes( - value for (index, value) in enumerate(arr) if index % 4 != 3 - ) - palette_length = self.im.putpalette(mode, arr) + self.im.putpalette(mode, arr) self.palette.dirty = 0 self.palette.rawmode = None if "transparency" in self.info and mode in ("LA", "PA"): @@ -837,8 +860,9 @@ class Image: self.im.putpalettealphas(self.info["transparency"]) self.palette.mode = "RGBA" else: - self.palette.mode = "RGB" - self.palette.palette = self.im.getpalette()[: palette_length * 3] + palette_mode = "RGBA" if mode.startswith("RGBA") else "RGB" + self.palette.mode = palette_mode + self.palette.palette = self.im.getpalette(palette_mode, palette_mode) if self.im: if cffi and USE_CFFI_ACCESS: @@ -862,7 +886,9 @@ class Image: """ pass - def convert(self, mode=None, matrix=None, dither=None, palette=WEB, colors=256): + def convert( + self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256 + ): """ Returns a converted copy of this image. For the "P" mode, this method translates pixels through the palette. If mode is @@ -881,7 +907,7 @@ class Image: The default method of converting a greyscale ("L") or "RGB" image into a bilevel (mode "1") image uses Floyd-Steinberg dither to approximate the original image luminosity levels. If - dither is :data:`NONE`, all values larger than 127 are set to 255 (white), + dither is ``None``, all values larger than 127 are set to 255 (white), all other values to 0 (black). To use other thresholds, use the :py:meth:`~PIL.Image.Image.point` method. @@ -894,12 +920,13 @@ class Image: should be 4- or 12-tuple containing floating point values. :param dither: Dithering method, used when converting from mode "RGB" to "P" or from "RGB" or "L" to "1". - Available methods are :data:`NONE` or :data:`FLOYDSTEINBERG` (default). - Note that this is not used when ``matrix`` is supplied. + Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` + (default). Note that this is not used when ``matrix`` is supplied. :param palette: Palette to use when converting from mode "RGB" - to "P". Available palettes are :data:`WEB` or :data:`ADAPTIVE`. - :param colors: Number of colors to use for the :data:`ADAPTIVE` palette. - Defaults to 256. + to "P". Available palettes are :data:`Palette.WEB` or + :data:`Palette.ADAPTIVE`. + :param colors: Number of colors to use for the :data:`Palette.ADAPTIVE` + palette. Defaults to 256. :rtype: :py:class:`~PIL.Image.Image` :returns: An :py:class:`~PIL.Image.Image` object. """ @@ -1006,7 +1033,7 @@ class Image: else: raise ValueError("Transparency for P mode should be bytes or int") - if mode == "P" and palette == ADAPTIVE: + if mode == "P" and palette == Palette.ADAPTIVE: im = self.im.quantize(colors) new = self._new(im) from . import ImagePalette @@ -1028,7 +1055,7 @@ class Image: # colorspace conversion if dither is None: - dither = FLOYDSTEINBERG + dither = Dither.FLOYDSTEINBERG try: im = self.im.convert(mode, dither) @@ -1041,7 +1068,7 @@ class Image: raise ValueError("illegal conversion") from e new_im = self._new(im) - if mode == "P" and palette != ADAPTIVE: + if mode == "P" and palette != Palette.ADAPTIVE: from . import ImagePalette new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) @@ -1064,31 +1091,38 @@ class Image: new_im.info["transparency"] = trns return new_im - def quantize(self, colors=256, method=None, kmeans=0, palette=None, dither=1): + def quantize( + self, + colors=256, + method=None, + kmeans=0, + palette=None, + dither=Dither.FLOYDSTEINBERG, + ): """ Convert the image to 'P' mode with the specified number of colors. :param colors: The desired number of colors, <= 256 - :param method: :data:`MEDIANCUT` (median cut), - :data:`MAXCOVERAGE` (maximum coverage), - :data:`FASTOCTREE` (fast octree), - :data:`LIBIMAGEQUANT` (libimagequant; check support using - :py:func:`PIL.features.check_feature` - with ``feature="libimagequant"``). + :param method: :data:`Quantize.MEDIANCUT` (median cut), + :data:`Quantize.MAXCOVERAGE` (maximum coverage), + :data:`Quantize.FASTOCTREE` (fast octree), + :data:`Quantize.LIBIMAGEQUANT` (libimagequant; check support + using :py:func:`PIL.features.check_feature` with + ``feature="libimagequant"``). - By default, :data:`MEDIANCUT` will be used. + By default, :data:`Quantize.MEDIANCUT` will be used. - The exception to this is RGBA images. :data:`MEDIANCUT` and - :data:`MAXCOVERAGE` do not support RGBA images, so - :data:`FASTOCTREE` is used by default instead. + The exception to this is RGBA images. :data:`Quantize.MEDIANCUT` + and :data:`Quantize.MAXCOVERAGE` do not support RGBA images, so + :data:`Quantize.FASTOCTREE` is used by default instead. :param kmeans: Integer :param palette: Quantize to the palette of given :py:class:`PIL.Image.Image`. :param dither: Dithering method, used when converting from mode "RGB" to "P" or from "RGB" or "L" to "1". - Available methods are :data:`NONE` or :data:`FLOYDSTEINBERG` (default). - Default: 1 (legacy setting) + Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` + (default). :returns: A new image """ @@ -1097,11 +1131,14 @@ class Image: if method is None: # defaults: - method = MEDIANCUT + method = Quantize.MEDIANCUT if self.mode == "RGBA": - method = FASTOCTREE + method = Quantize.FASTOCTREE - if self.mode == "RGBA" and method not in (FASTOCTREE, LIBIMAGEQUANT): + if self.mode == "RGBA" and method not in ( + Quantize.FASTOCTREE, + Quantize.LIBIMAGEQUANT, + ): # Caller specified an invalid mode. raise ValueError( "Fast Octree (method == 2) and libimagequant (method == 3) " @@ -1404,19 +1441,27 @@ class Image: self.load() return self.im.ptr - def getpalette(self): + def getpalette(self, rawmode="RGB"): """ Returns the image palette as a list. + :param rawmode: The mode in which to return the palette. ``None`` will + return the palette in its current mode. + + .. versionadded:: 9.1.0 + :returns: A list of color values [r, g, b, ...], or None if the image has no palette. """ self.load() try: - return list(self.im.getpalette()) + mode = self.im.getpalettemode() except ValueError: return None # no palette + if rawmode is None: + rawmode = mode + return list(self.im.getpalette(mode, rawmode)) def getpixel(self, xy): """ @@ -1761,8 +1806,8 @@ class Image: Alternatively, an 8-bit string may be used instead of an integer sequence. :param data: A palette sequence (either a list or a string). - :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a - mode that can be transformed to "RGB" (e.g. "R", "BGR;15", "RGBA;L"). + :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode + that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L"). """ from . import ImagePalette @@ -1911,15 +1956,18 @@ class Image: :param size: The requested size in pixels, as a 2-tuple: (width, height). :param resample: An optional resampling filter. This can be - one of :py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BOX`, - :py:data:`PIL.Image.BILINEAR`, :py:data:`PIL.Image.HAMMING`, - :py:data:`PIL.Image.BICUBIC` or :py:data:`PIL.Image.LANCZOS`. + one of :py:data:`PIL.Image.Resampling.NEAREST`, + :py:data:`PIL.Image.Resampling.BOX`, + :py:data:`PIL.Image.Resampling.BILINEAR`, + :py:data:`PIL.Image.Resampling.HAMMING`, + :py:data:`PIL.Image.Resampling.BICUBIC` or + :py:data:`PIL.Image.Resampling.LANCZOS`. If the image has mode "1" or "P", it is always set to - :py:data:`PIL.Image.NEAREST`. + :py:data:`PIL.Image.Resampling.NEAREST`. If the image mode specifies a number of bits, such as "I;16", then the - default filter is :py:data:`PIL.Image.NEAREST`. - Otherwise, the default filter is :py:data:`PIL.Image.BICUBIC`. - See: :ref:`concept-filters`. + default filter is :py:data:`PIL.Image.Resampling.NEAREST`. + Otherwise, the default filter is + :py:data:`PIL.Image.Resampling.BICUBIC`. See: :ref:`concept-filters`. :param box: An optional 4-tuple of floats providing the source image region to be scaled. The values must be within (0, 0, width, height) rectangle. @@ -1941,19 +1989,26 @@ class Image: if resample is None: type_special = ";" in self.mode - resample = NEAREST if type_special else BICUBIC - elif resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING): + resample = Resampling.NEAREST if type_special else Resampling.BICUBIC + elif resample not in ( + Resampling.NEAREST, + Resampling.BILINEAR, + Resampling.BICUBIC, + Resampling.LANCZOS, + Resampling.BOX, + Resampling.HAMMING, + ): message = f"Unknown resampling filter ({resample})." filters = [ f"{filter[1]} ({filter[0]})" for filter in ( - (NEAREST, "Image.NEAREST"), - (LANCZOS, "Image.LANCZOS"), - (BILINEAR, "Image.BILINEAR"), - (BICUBIC, "Image.BICUBIC"), - (BOX, "Image.BOX"), - (HAMMING, "Image.HAMMING"), + (Resampling.NEAREST, "Image.Resampling.NEAREST"), + (Resampling.LANCZOS, "Image.Resampling.LANCZOS"), + (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), + (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), + (Resampling.BOX, "Image.Resampling.BOX"), + (Resampling.HAMMING, "Image.Resampling.HAMMING"), ) ] raise ValueError( @@ -1974,16 +2029,16 @@ class Image: return self.copy() if self.mode in ("1", "P"): - resample = NEAREST + resample = Resampling.NEAREST - if self.mode in ["LA", "RGBA"] and resample != NEAREST: + if self.mode in ["LA", "RGBA"] and resample != Resampling.NEAREST: im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) im = im.resize(size, resample, box) return im.convert(self.mode) self.load() - if reducing_gap is not None and resample != NEAREST: + if reducing_gap is not None and resample != Resampling.NEAREST: factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 if factor_x > 1 or factor_y > 1: @@ -2038,7 +2093,7 @@ class Image: def rotate( self, angle, - resample=NEAREST, + resample=Resampling.NEAREST, expand=0, center=None, translate=None, @@ -2051,12 +2106,12 @@ class Image: :param angle: In degrees counter clockwise. :param resample: An optional resampling filter. This can be - one of :py:data:`PIL.Image.NEAREST` (use nearest neighbour), + one of :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour), :py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 - environment), or :py:data:`PIL.Image.BICUBIC` + environment), or :py:data:`PIL.Image.Resampling.BICUBIC` (cubic spline interpolation in a 4x4 environment). If omitted, or if the image has mode "1" or "P", it is - set to :py:data:`PIL.Image.NEAREST`. See :ref:`concept-filters`. + set to :py:data:`PIL.Image.Resampling.NEAREST`. See :ref:`concept-filters`. :param expand: Optional expansion flag. If true, expands the output image to make it large enough to hold the entire rotated image. If false or omitted, make the output image the same size as the @@ -2077,9 +2132,11 @@ class Image: if angle == 0: return self.copy() if angle == 180: - return self.transpose(ROTATE_180) + return self.transpose(Transpose.ROTATE_180) if angle in (90, 270) and (expand or self.width == self.height): - return self.transpose(ROTATE_90 if angle == 90 else ROTATE_270) + return self.transpose( + Transpose.ROTATE_90 if angle == 90 else Transpose.ROTATE_270 + ) # Calculate the affine matrix. Note that this is the reverse # transformation (from destination image to source) because we @@ -2148,7 +2205,9 @@ class Image: matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix) w, h = nw, nh - return self.transform((w, h), AFFINE, matrix, resample, fillcolor=fillcolor) + return self.transform( + (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor + ) def save(self, fp, format=None, **params): """ @@ -2334,7 +2393,7 @@ class Image: """ return 0 - def thumbnail(self, size, resample=BICUBIC, reducing_gap=2.0): + def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0): """ Make this image into a thumbnail. This method modifies the image to contain a thumbnail version of itself, no larger than @@ -2350,11 +2409,14 @@ class Image: :param size: Requested size. :param resample: Optional resampling filter. This can be one - of :py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BOX`, - :py:data:`PIL.Image.BILINEAR`, :py:data:`PIL.Image.HAMMING`, - :py:data:`PIL.Image.BICUBIC` or :py:data:`PIL.Image.LANCZOS`. - If omitted, it defaults to :py:data:`PIL.Image.BICUBIC`. - (was :py:data:`PIL.Image.NEAREST` prior to version 2.5.0). + of :py:data:`PIL.Image.Resampling.NEAREST`, + :py:data:`PIL.Image.Resampling.BOX`, + :py:data:`PIL.Image.Resampling.BILINEAR`, + :py:data:`PIL.Image.Resampling.HAMMING`, + :py:data:`PIL.Image.Resampling.BICUBIC` or + :py:data:`PIL.Image.Resampling.LANCZOS`. + If omitted, it defaults to :py:data:`PIL.Image.Resampling.BICUBIC`. + (was :py:data:`PIL.Image.Resampling.NEAREST` prior to version 2.5.0). See: :ref:`concept-filters`. :param reducing_gap: Apply optimization by resizing the image in two steps. First, reducing the image by integer times @@ -2409,7 +2471,13 @@ class Image: # FIXME: the different transform methods need further explanation # instead of bloating the method docs, add a separate chapter. def transform( - self, size, method, data=None, resample=NEAREST, fill=1, fillcolor=None + self, + size, + method, + data=None, + resample=Resampling.NEAREST, + fill=1, + fillcolor=None, ): """ Transforms this image. This method creates a new image with the @@ -2418,11 +2486,11 @@ class Image: :param size: The output size. :param method: The transformation method. This is one of - :py:data:`PIL.Image.EXTENT` (cut out a rectangular subregion), - :py:data:`PIL.Image.AFFINE` (affine transform), - :py:data:`PIL.Image.PERSPECTIVE` (perspective transform), - :py:data:`PIL.Image.QUAD` (map a quadrilateral to a rectangle), or - :py:data:`PIL.Image.MESH` (map a number of source quadrilaterals + :py:data:`PIL.Image.Transform.EXTENT` (cut out a rectangular subregion), + :py:data:`PIL.Image.Transform.AFFINE` (affine transform), + :py:data:`PIL.Image.Transform.PERSPECTIVE` (perspective transform), + :py:data:`PIL.Image.Transform.QUAD` (map a quadrilateral to a rectangle), or + :py:data:`PIL.Image.Transform.MESH` (map a number of source quadrilaterals in one operation). It may also be an :py:class:`~PIL.Image.ImageTransformHandler` @@ -2437,16 +2505,16 @@ class Image: class Example: def getdata(self): - method = Image.EXTENT + method = Image.Transform.EXTENT data = (0, 0, 100, 100) return method, data :param data: Extra data to the transformation method. :param resample: Optional resampling filter. It can be one of - :py:data:`PIL.Image.NEAREST` (use nearest neighbour), - :py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 + :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour), + :py:data:`PIL.Image.Resampling.BILINEAR` (linear interpolation in a 2x2 environment), or :py:data:`PIL.Image.BICUBIC` (cubic spline interpolation in a 4x4 environment). If omitted, or if the image - has mode "1" or "P", it is set to :py:data:`PIL.Image.NEAREST`. + has mode "1" or "P", it is set to :py:data:`PIL.Image.Resampling.NEAREST`. See: :ref:`concept-filters`. :param fill: If ``method`` is an :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of @@ -2456,7 +2524,7 @@ class Image: :returns: An :py:class:`~PIL.Image.Image` object. """ - if self.mode in ("LA", "RGBA") and resample != NEAREST: + if self.mode in ("LA", "RGBA") and resample != Resampling.NEAREST: return ( self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) .transform(size, method, data, resample, fill, fillcolor) @@ -2477,10 +2545,12 @@ class Image: if self.mode == "P" and self.palette: im.palette = self.palette.copy() im.info = self.info.copy() - if method == MESH: + if method == Transform.MESH: # list of quads for box, quad in data: - im.__transformer(box, self, QUAD, quad, resample, fillcolor is None) + im.__transformer( + box, self, Transform.QUAD, quad, resample, fillcolor is None + ) else: im.__transformer( (0, 0) + size, self, method, data, resample, fillcolor is None @@ -2488,25 +2558,27 @@ class Image: return im - def __transformer(self, box, image, method, data, resample=NEAREST, fill=1): + def __transformer( + self, box, image, method, data, resample=Resampling.NEAREST, fill=1 + ): w = box[2] - box[0] h = box[3] - box[1] - if method == AFFINE: + if method == Transform.AFFINE: data = data[0:6] - elif method == EXTENT: + elif method == Transform.EXTENT: # convert extent to an affine transform x0, y0, x1, y1 = data xs = (x1 - x0) / w ys = (y1 - y0) / h - method = AFFINE + method = Transform.AFFINE data = (xs, 0, x0, 0, ys, y0) - elif method == PERSPECTIVE: + elif method == Transform.PERSPECTIVE: data = data[0:8] - elif method == QUAD: + elif method == Transform.QUAD: # quadrilateral warp. data specifies the four corners # given as NW, SW, SE, and NE. nw = data[0:2] @@ -2530,12 +2602,16 @@ class Image: else: raise ValueError("unknown transformation method") - if resample not in (NEAREST, BILINEAR, BICUBIC): - if resample in (BOX, HAMMING, LANCZOS): + if resample not in ( + Resampling.NEAREST, + Resampling.BILINEAR, + Resampling.BICUBIC, + ): + if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): message = { - BOX: "Image.BOX", - HAMMING: "Image.HAMMING", - LANCZOS: "Image.LANCZOS/Image.ANTIALIAS", + Resampling.BOX: "Image.Resampling.BOX", + Resampling.HAMMING: "Image.Resampling.HAMMING", + Resampling.LANCZOS: "Image.Resampling.LANCZOS", }[resample] + f" ({resample}) cannot be used." else: message = f"Unknown resampling filter ({resample})." @@ -2543,9 +2619,9 @@ class Image: filters = [ f"{filter[1]} ({filter[0]})" for filter in ( - (NEAREST, "Image.NEAREST"), - (BILINEAR, "Image.BILINEAR"), - (BICUBIC, "Image.BICUBIC"), + (Resampling.NEAREST, "Image.Resampling.NEAREST"), + (Resampling.BILINEAR, "Image.Resampling.BILINEAR"), + (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), ) ] raise ValueError( @@ -2557,7 +2633,7 @@ class Image: self.load() if image.mode in ("1", "P"): - resample = NEAREST + resample = Resampling.NEAREST self.im.transform2(box, image.im, method, data, resample, fill) @@ -2565,10 +2641,13 @@ class Image: """ Transpose image (flip or rotate in 90 degree steps) - :param method: One of :py:data:`PIL.Image.FLIP_LEFT_RIGHT`, - :py:data:`PIL.Image.FLIP_TOP_BOTTOM`, :py:data:`PIL.Image.ROTATE_90`, - :py:data:`PIL.Image.ROTATE_180`, :py:data:`PIL.Image.ROTATE_270`, - :py:data:`PIL.Image.TRANSPOSE` or :py:data:`PIL.Image.TRANSVERSE`. + :param method: One of :py:data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT`, + :py:data:`PIL.Image.Transpose.FLIP_TOP_BOTTOM`, + :py:data:`PIL.Image.Transpose.ROTATE_90`, + :py:data:`PIL.Image.Transpose.ROTATE_180`, + :py:data:`PIL.Image.Transpose.ROTATE_270`, + :py:data:`PIL.Image.Transpose.TRANSPOSE` or + :py:data:`PIL.Image.Transpose.TRANSVERSE`. :returns: Returns a flipped or rotated copy of this image. """ diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 60e700f09..ea328e149 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -16,6 +16,8 @@ # below for the original description. import sys +import warnings +from enum import IntEnum from PIL import Image @@ -100,14 +102,42 @@ core = _imagingcms # # intent/direction values -INTENT_PERCEPTUAL = 0 -INTENT_RELATIVE_COLORIMETRIC = 1 -INTENT_SATURATION = 2 -INTENT_ABSOLUTE_COLORIMETRIC = 3 -DIRECTION_INPUT = 0 -DIRECTION_OUTPUT = 1 -DIRECTION_PROOF = 2 +class Intent(IntEnum): + PERCEPTUAL = 0 + RELATIVE_COLORIMETRIC = 1 + SATURATION = 2 + ABSOLUTE_COLORIMETRIC = 3 + + +class Direction(IntEnum): + INPUT = 0 + OUTPUT = 1 + PROOF = 2 + + +def __getattr__(name): + deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). " + for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items(): + if name.startswith(prefix): + name = name[len(prefix) :] + if name in enum.__members__: + warnings.warn( + prefix + + name + + " is " + + deprecated + + "Use " + + enum.__name__ + + "." + + name + + " instead.", + DeprecationWarning, + stacklevel=2, + ) + return enum[name] + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + # # flags @@ -211,9 +241,9 @@ class ImageCmsTransform(Image.ImagePointHandler): output, input_mode, output_mode, - intent=INTENT_PERCEPTUAL, + intent=Intent.PERCEPTUAL, proof=None, - proof_intent=INTENT_ABSOLUTE_COLORIMETRIC, + proof_intent=Intent.ABSOLUTE_COLORIMETRIC, flags=0, ): if proof is None: @@ -295,7 +325,7 @@ def profileToProfile( im, inputProfile, outputProfile, - renderingIntent=INTENT_PERCEPTUAL, + renderingIntent=Intent.PERCEPTUAL, outputMode=None, inPlace=False, flags=0, @@ -331,10 +361,10 @@ def profileToProfile( :param renderingIntent: Integer (0-3) specifying the rendering intent you wish to use for the transform - ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) - ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 - ImageCms.INTENT_SATURATION = 2 - ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. @@ -412,7 +442,7 @@ def buildTransform( outputProfile, inMode, outMode, - renderingIntent=INTENT_PERCEPTUAL, + renderingIntent=Intent.PERCEPTUAL, flags=0, ): """ @@ -458,10 +488,10 @@ def buildTransform( :param renderingIntent: Integer (0-3) specifying the rendering intent you wish to use for the transform - ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) - ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 - ImageCms.INTENT_SATURATION = 2 - ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. @@ -494,8 +524,8 @@ def buildProofTransform( proofProfile, inMode, outMode, - renderingIntent=INTENT_PERCEPTUAL, - proofRenderingIntent=INTENT_ABSOLUTE_COLORIMETRIC, + renderingIntent=Intent.PERCEPTUAL, + proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC, flags=FLAGS["SOFTPROOFING"], ): """ @@ -550,20 +580,20 @@ def buildProofTransform( :param renderingIntent: Integer (0-3) specifying the rendering intent you wish to use for the input->proof (simulated) transform - ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) - ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 - ImageCms.INTENT_SATURATION = 2 - ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. :param proofRenderingIntent: Integer (0-3) specifying the rendering intent you wish to use for proof->output transform - ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) - ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 - ImageCms.INTENT_SATURATION = 2 - ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. @@ -922,10 +952,10 @@ def getDefaultIntent(profile): :returns: Integer 0-3 specifying the default rendering intent for this profile. - ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) - ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 - ImageCms.INTENT_SATURATION = 2 - ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. @@ -960,19 +990,19 @@ def isIntentSupported(profile, intent, direction): :param intent: Integer (0-3) specifying the rendering intent you wish to use with this profile - ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) - ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 - ImageCms.INTENT_SATURATION = 2 - ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 + ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT) + ImageCms.Intent.RELATIVE_COLORIMETRIC = 1 + ImageCms.Intent.SATURATION = 2 + ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. :param direction: Integer specifying if the profile is to be used for input, output, or proof - INPUT = 0 (or use ImageCms.DIRECTION_INPUT) - OUTPUT = 1 (or use ImageCms.DIRECTION_OUTPUT) - PROOF = 2 (or use ImageCms.DIRECTION_PROOF) + INPUT = 0 (or use ImageCms.Direction.INPUT) + OUTPUT = 1 (or use ImageCms.Direction.OUTPUT) + PROOF = 2 (or use ImageCms.Direction.PROOF) :returns: 1 if the intent/direction are supported, -1 if they are not. :exception PyCMSError: diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 331410f0e..767b38ca4 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -49,7 +49,11 @@ ERRORS = { -8: "bad configuration", -9: "out of memory error", } -"""Dict of known error codes returned from :meth:`.PyDecoder.decode`.""" +""" +Dict of known error codes returned from :meth:`.PyDecoder.decode`, +:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and +:meth:`.PyEncoder.encode_to_file`. +""" # @@ -577,16 +581,7 @@ class PyCodecState: return (self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize) -class PyDecoder: - """ - Python implementation of a format decoder. Override this class and - add the decoding logic in the :meth:`decode` method. - - See :ref:`Writing Your Own File Decoder in Python` - """ - - _pulls_fd = False - +class PyCodec: def __init__(self, mode, *args): self.im = None self.state = PyCodecState() @@ -596,31 +591,16 @@ class PyDecoder: def init(self, args): """ - Override to perform decoder specific initialization + Override to perform codec specific initialization :param args: Array of args items from the tile entry :returns: None """ self.args = args - @property - def pulls_fd(self): - return self._pulls_fd - - def decode(self, buffer): - """ - Override to perform the decoding process. - - :param buffer: A bytes object with the data to be decoded. - :returns: A tuple of ``(bytes consumed, errcode)``. - If finished with decoding return <0 for the bytes consumed. - Err codes are from :data:`.ImageFile.ERRORS`. - """ - raise NotImplementedError() - def cleanup(self): """ - Override to perform decoder specific cleanup + Override to perform codec specific cleanup :returns: None """ @@ -628,16 +608,16 @@ class PyDecoder: def setfd(self, fd): """ - Called from ImageFile to set the python file-like object + Called from ImageFile to set the Python file-like object - :param fd: A python file-like object + :param fd: A Python file-like object :returns: None """ self.fd = fd def setimage(self, im, extents=None): """ - Called from ImageFile to set the core output image for the decoder + Called from ImageFile to set the core output image for the codec :param im: A core image object :param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle @@ -670,6 +650,32 @@ class PyDecoder: ): raise ValueError("Tile cannot extend outside image") + +class PyDecoder(PyCodec): + """ + Python implementation of a format decoder. Override this class and + add the decoding logic in the :meth:`decode` method. + + See :ref:`Writing Your Own File Codec in Python` + """ + + _pulls_fd = False + + @property + def pulls_fd(self): + return self._pulls_fd + + def decode(self, buffer): + """ + Override to perform the decoding process. + + :param buffer: A bytes object with the data to be decoded. + :returns: A tuple of ``(bytes consumed, errcode)``. + If finished with decoding return 0 for the bytes consumed. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + raise NotImplementedError() + def set_as_raw(self, data, rawmode=None): """ Convenience method to set the internal image from a stream of raw data @@ -690,3 +696,57 @@ class PyDecoder: raise ValueError("not enough image data") if s[1] != 0: raise ValueError("cannot decode image data") + + +class PyEncoder(PyCodec): + """ + Python implementation of a format encoder. Override this class and + add the decoding logic in the :meth:`encode` method. + + See :ref:`Writing Your Own File Codec in Python` + """ + + _pushes_fd = False + + @property + def pushes_fd(self): + return self._pushes_fd + + def encode(self, bufsize): + """ + Override to perform the encoding process. + + :param bufsize: Buffer size. + :returns: A tuple of ``(bytes encoded, errcode, bytes)``. + If finished with encoding return 1 for the error code. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + raise NotImplementedError() + + def encode_to_pyfd(self): + """ + :returns: A tuple of ``(bytes consumed, errcode)``. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + if not self.pushes_fd: + return 0, -8 # bad configuration + bytes_consumed, errcode, data = self.encode(0) + if data: + self.fd.write(data) + return bytes_consumed, errcode + + def encode_to_file(self, fh, bufsize): + """ + :param fh: File handle. + :param bufsize: Buffer size. + + :returns: If finished successfully, return 0. + Otherwise, return an error code. Err codes are from + :data:`.ImageFile.ERRORS`. + """ + errcode = 0 + while errcode == 0: + status, errcode, buf = self.encode(bufsize) + if status > 0: + fh.write(buf[status:]) + return errcode diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index d2ece3752..1320af8f9 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -529,7 +529,7 @@ class Color3DLUT(MultibandFilter): return image.color_lut_3d( self.mode or image.mode, - Image.LINEAR, + Image.Resampling.BILINEAR, self.channels, self.size[0], self.size[1], diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 805c8fff9..f21b6de71 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -28,13 +28,40 @@ import base64 import os import sys +import warnings +from enum import IntEnum from io import BytesIO from . import Image from ._util import isDirectory, isPath -LAYOUT_BASIC = 0 -LAYOUT_RAQM = 1 + +class Layout(IntEnum): + BASIC = 0 + RAQM = 1 + + +def __getattr__(name): + deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). " + for enum, prefix in {Layout: "LAYOUT_"}.items(): + if name.startswith(prefix): + name = name[len(prefix) :] + if name in enum.__members__: + warnings.warn( + prefix + + name + + " is " + + deprecated + + "Use " + + enum.__name__ + + "." + + name + + " instead.", + DeprecationWarning, + stacklevel=2, + ) + return enum[name] + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") class _imagingft_not_installed: @@ -164,12 +191,18 @@ class FreeTypeFont: self.index = index self.encoding = encoding - if layout_engine not in (LAYOUT_BASIC, LAYOUT_RAQM): - layout_engine = LAYOUT_BASIC + if layout_engine not in (Layout.BASIC, Layout.RAQM): + layout_engine = Layout.BASIC if core.HAVE_RAQM: - layout_engine = LAYOUT_RAQM - elif layout_engine == LAYOUT_RAQM and not core.HAVE_RAQM: - layout_engine = LAYOUT_BASIC + layout_engine = Layout.RAQM + elif layout_engine == Layout.RAQM and not core.HAVE_RAQM: + import warnings + + warnings.warn( + "Raqm layout was requested, but Raqm is not available. " + "Falling back to basic layout." + ) + layout_engine = Layout.BASIC self.layout_engine = layout_engine @@ -751,15 +784,16 @@ class TransposedFont: :param font: A font object. :param orientation: An optional orientation. If given, this should - be one of Image.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM, - Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270. + be one of Image.Transpose.FLIP_LEFT_RIGHT, Image.Transpose.FLIP_TOP_BOTTOM, + Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_180, or + Image.Transpose.ROTATE_270. """ self.font = font self.orientation = orientation # any 'transpose' argument, or None def getsize(self, text, *args, **kwargs): w, h = self.font.getsize(text) - if self.orientation in (Image.ROTATE_90, Image.ROTATE_270): + if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): return h, w return w, h @@ -805,7 +839,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): :file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on macOS. - :param size: The requested size, in points. + :param size: The requested size, in pixels. :param index: Which font face to load (default is first available face). :param encoding: Which font encoding to use (default is Unicode). Possible encodings include (see the FreeType documentation for more @@ -827,7 +861,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): This specifies the character set to use. It does not alter the encoding of any text provided in subsequent operations. :param layout_engine: Which layout engine to use, if available: - :data:`.ImageFont.LAYOUT_BASIC` or :data:`.ImageFont.LAYOUT_RAQM`. + :data:`.ImageFont.Layout.BASIC` or :data:`.ImageFont.Layout.RAQM`. You can check support for Raqm layout using :py:func:`PIL.features.check_feature` with ``feature="raqm"``. diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index e6bf0bb10..0973536c9 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -13,6 +13,8 @@ # See the README file for information on usage and redistribution. # +import sys + # mode descriptor cache _modes = None @@ -20,11 +22,12 @@ _modes = None class ModeDescriptor: """Wrapper for mode strings.""" - def __init__(self, mode, bands, basemode, basetype): + def __init__(self, mode, bands, basemode, basetype, typestr): self.mode = mode self.bands = bands self.basemode = basemode self.basetype = basetype + self.typestr = typestr def __str__(self): return self.mode @@ -36,43 +39,53 @@ def getmode(mode): if not _modes: # initialize mode cache modes = {} - for m, (basemode, basetype, bands) in { + endian = "<" if sys.byteorder == "little" else ">" + for m, (basemode, basetype, bands, typestr) in { # core modes - "1": ("L", "L", ("1",)), - "L": ("L", "L", ("L",)), - "I": ("L", "I", ("I",)), - "F": ("L", "F", ("F",)), - "P": ("P", "L", ("P",)), - "RGB": ("RGB", "L", ("R", "G", "B")), - "RGBX": ("RGB", "L", ("R", "G", "B", "X")), - "RGBA": ("RGB", "L", ("R", "G", "B", "A")), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), - "LAB": ("RGB", "L", ("L", "A", "B")), - "HSV": ("RGB", "L", ("H", "S", "V")), + # Bits need to be extended to bytes + "1": ("L", "L", ("1",), "|b1"), + "L": ("L", "L", ("L",), "|u1"), + "I": ("L", "I", ("I",), endian + "i4"), + "F": ("L", "F", ("F",), endian + "f4"), + "P": ("P", "L", ("P",), "|u1"), + "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), + "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), + "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"), + "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"), + "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"), + # UNDONE - unsigned |u1i1i1 + "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"), + "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), # extra experimental modes - "RGBa": ("RGB", "L", ("R", "G", "B", "a")), - "BGR;15": ("RGB", "L", ("B", "G", "R")), - "BGR;16": ("RGB", "L", ("B", "G", "R")), - "BGR;24": ("RGB", "L", ("B", "G", "R")), - "BGR;32": ("RGB", "L", ("B", "G", "R")), - "LA": ("L", "L", ("L", "A")), - "La": ("L", "L", ("L", "a")), - "PA": ("RGB", "L", ("P", "A")), + "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), + "BGR;15": ("RGB", "L", ("B", "G", "R"), endian + "u2"), + "BGR;16": ("RGB", "L", ("B", "G", "R"), endian + "u2"), + "BGR;24": ("RGB", "L", ("B", "G", "R"), endian + "u3"), + "BGR;32": ("RGB", "L", ("B", "G", "R"), endian + "u4"), + "LA": ("L", "L", ("L", "A"), "|u1"), + "La": ("L", "L", ("L", "a"), "|u1"), + "PA": ("RGB", "L", ("P", "A"), "|u1"), }.items(): - modes[m] = ModeDescriptor(m, bands, basemode, basetype) + modes[m] = ModeDescriptor(m, bands, basemode, basetype, typestr) # mapping modes - for i16mode in ( - "I;16", - "I;16S", - "I;16L", - "I;16LS", - "I;16B", - "I;16BS", - "I;16N", - "I;16NS", - ): - modes[i16mode] = ModeDescriptor(i16mode, ("I",), "L", "L") + for i16mode, typestr in { + # I;16 == I;16L, and I;32 == I;32L + "I;16": "u2", + "I;16BS": ">i2", + "I;16N": endian + "u2", + "I;16NS": endian + "i2", + "I;32": "u4", + "I;32L": "i4", + "I;32LS": "`. # APNG frame disposal modes -APNG_DISPOSE_OP_NONE = 0 -""" -No disposal is done on this frame before rendering the next frame. -See :ref:`Saving APNG sequences`. -""" -APNG_DISPOSE_OP_BACKGROUND = 1 -""" -This frame’s modified region is cleared to fully transparent black before rendering -the next frame. -See :ref:`Saving APNG sequences`. -""" -APNG_DISPOSE_OP_PREVIOUS = 2 -""" -This frame’s modified region is reverted to the previous frame’s contents before -rendering the next frame. -See :ref:`Saving APNG sequences`. -""" +class Disposal(IntEnum): + OP_NONE = 0 + """ + No disposal is done on this frame before rendering the next frame. + See :ref:`Saving APNG sequences`. + """ + OP_BACKGROUND = 1 + """ + This frame’s modified region is cleared to fully transparent black before rendering + the next frame. + See :ref:`Saving APNG sequences`. + """ + OP_PREVIOUS = 2 + """ + This frame’s modified region is reverted to the previous frame’s contents before + rendering the next frame. + See :ref:`Saving APNG sequences`. + """ + # APNG frame blend modes -APNG_BLEND_OP_SOURCE = 0 -""" -All color components of this frame, including alpha, overwrite the previous output -image contents. -See :ref:`Saving APNG sequences`. -""" -APNG_BLEND_OP_OVER = 1 -""" -This frame should be alpha composited with the previous output image contents. -See :ref:`Saving APNG sequences`. -""" +class Blend(IntEnum): + OP_SOURCE = 0 + """ + All color components of this frame, including alpha, overwrite the previous output + image contents. + See :ref:`Saving APNG sequences`. + """ + OP_OVER = 1 + """ + This frame should be alpha composited with the previous output image contents. + See :ref:`Saving APNG sequences`. + """ + + +def __getattr__(name): + deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). " + for enum, prefix in {Disposal: "APNG_DISPOSE_", Blend: "APNG_BLEND_"}.items(): + if name.startswith(prefix): + name = name[len(prefix) :] + if name in enum.__members__: + warnings.warn( + prefix + + name + + " is " + + deprecated + + "Use " + + enum.__name__ + + "." + + name + + " instead.", + DeprecationWarning, + stacklevel=2, + ) + return enum[name] + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") def _safe_zlib_decompress(s): @@ -861,13 +888,13 @@ class PngImageFile(ImageFile.ImageFile): raise EOFError # setup frame disposal (actual disposal done when needed in the next _seek()) - if self._prev_im is None and self.dispose_op == APNG_DISPOSE_OP_PREVIOUS: - self.dispose_op = APNG_DISPOSE_OP_BACKGROUND + if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: + self.dispose_op = Disposal.OP_BACKGROUND - if self.dispose_op == APNG_DISPOSE_OP_PREVIOUS: + if self.dispose_op == Disposal.OP_PREVIOUS: self.dispose = self._prev_im.copy() self.dispose = self._crop(self.dispose, self.dispose_extent) - elif self.dispose_op == APNG_DISPOSE_OP_BACKGROUND: + elif self.dispose_op == Disposal.OP_BACKGROUND: self.dispose = Image.core.fill(self.mode, self.size) self.dispose = self._crop(self.dispose, self.dispose_extent) else: @@ -956,7 +983,7 @@ class PngImageFile(ImageFile.ImageFile): self.png.close() self.png = None else: - if self._prev_im and self.blend_op == APNG_BLEND_OP_OVER: + if self._prev_im and self.blend_op == Blend.OP_OVER: updated = self._crop(self.im, self.dispose_extent) self._prev_im.paste( updated, self.dispose_extent, updated.convert("RGBA") @@ -982,6 +1009,7 @@ class PngImageFile(ImageFile.ImageFile): """ Returns a dictionary containing the XMP tags. Requires defusedxml to be installed. + :returns: XMP tags in a dictionary. """ return ( @@ -1061,10 +1089,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode): default_image = im.encoderinfo.get("default_image", im.info.get("default_image")) duration = im.encoderinfo.get("duration", im.info.get("duration", 0)) loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) - disposal = im.encoderinfo.get( - "disposal", im.info.get("disposal", APNG_DISPOSE_OP_NONE) - ) - blend = im.encoderinfo.get("blend", im.info.get("blend", APNG_BLEND_OP_SOURCE)) + disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) + blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE)) if default_image: chain = itertools.chain(im.encoderinfo.get("append_images", [])) @@ -1094,10 +1120,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode): previous = im_frames[-1] prev_disposal = previous["encoderinfo"].get("disposal") prev_blend = previous["encoderinfo"].get("blend") - if prev_disposal == APNG_DISPOSE_OP_PREVIOUS and len(im_frames) < 2: - prev_disposal = APNG_DISPOSE_OP_BACKGROUND + if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2: + prev_disposal = Disposal.OP_BACKGROUND - if prev_disposal == APNG_DISPOSE_OP_BACKGROUND: + if prev_disposal == Disposal.OP_BACKGROUND: base_im = previous["im"] dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) bbox = previous["bbox"] @@ -1106,7 +1132,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode): else: bbox = (0, 0) + im.size base_im.paste(dispose, bbox) - elif prev_disposal == APNG_DISPOSE_OP_PREVIOUS: + elif prev_disposal == Disposal.OP_PREVIOUS: base_im = im_frames[-2]["im"] else: base_im = previous["im"] diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index abf4d651d..9d32927d4 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -49,26 +49,46 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" - def _token(self, s=b""): - while True: # read until next whitespace + def _read_magic(self): + magic = b"" + # read until whitespace or longest available magic number + for _ in range(6): c = self.fp.read(1) if not c or c in b_whitespace: break - if c > b"\x79": - raise ValueError("Expected ASCII value, found binary") - s = s + c - if len(s) > 9: - raise ValueError("Expected int, got > 9 digits") - return s + magic += c + return magic + + def _read_token(self): + token = b"" + while len(token) <= 10: # read until next whitespace or limit of 10 characters + c = self.fp.read(1) + if not c: + break + elif c in b_whitespace: # token ended + if not token: + # skip whitespace at start + continue + break + elif c == b"#": + # ignores rest of the line; stops at CR, LF or EOF + while self.fp.read(1) not in b"\r\n": + pass + continue + token += c + if not token: + # Token was not even 1 byte + raise ValueError("Reached EOF while reading header") + elif len(token) > 10: + raise ValueError(f"Token too long in file header: {token}") + return token def _open(self): - - # check magic - s = self.fp.read(1) - if s != b"P": + magic_number = self._read_magic() + try: + mode = MODES[magic_number] + except KeyError: raise SyntaxError("not a PPM file") - magic_number = self._token(s) - mode = MODES[magic_number] self.custom_mimetype = { b"P4": "image/x-portable-bitmap", @@ -83,29 +103,19 @@ class PpmImageFile(ImageFile.ImageFile): self.mode = rawmode = mode for ix in range(3): - while True: - while True: - s = self.fp.read(1) - if s not in b_whitespace: - break - if s == b"": - raise ValueError("File does not extend beyond magic number") - if s != b"#": - break - s = self.fp.readline() - s = int(self._token(s)) - if ix == 0: - xsize = s - elif ix == 1: - ysize = s + token = int(self._read_token()) + if ix == 0: # token is the x size + xsize = token + elif ix == 1: # token is the y size + ysize = token if mode == "1": break - elif ix == 2: - # maxgrey - if s > 255: + elif ix == 2: # token is maxval + maxval = token + if maxval > 255: if not mode == "L": - raise ValueError(f"Too many colors for band: {s}") - if s < 2 ** 16: + raise ValueError(f"Too many colors for band: {token}") + if maxval < 2 ** 16: self.mode = "I" rawmode = "I;16B" else: diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 11c706b4d..1a72f5c04 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -314,7 +314,7 @@ if __name__ == "__main__": outfile = sys.argv[2] # perform some image operation - im = im.transpose(Image.FLIP_LEFT_RIGHT) + im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) print( f"saving a flipped version of {os.path.basename(filename)} " f"as {outfile} " diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index ed63da95f..59b89e988 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -152,7 +152,7 @@ class TgaImageFile(ImageFile.ImageFile): def load_end(self): if self._flip_horizontally: - self.im = self.im.transpose(Image.FLIP_LEFT_RIGHT) + self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) # diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index e54082fec..35ff1c1bb 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -41,6 +41,7 @@ import io import itertools import logging +import math import os import struct import warnings @@ -49,6 +50,8 @@ from fractions import Fraction from numbers import Number, Rational from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags +from ._binary import i16be as i16 +from ._binary import i32be as i32 from ._binary import o8 from .TiffTags import TYPES @@ -1124,10 +1127,32 @@ class TiffImageFile(ImageFile.ImageFile): """ Returns a dictionary containing the XMP tags. Requires defusedxml to be installed. + :returns: XMP tags in a dictionary. """ return self._getxmp(self.tag_v2[700]) if 700 in self.tag_v2 else {} + def get_photoshop_blocks(self): + """ + Returns a dictionary of Photoshop "Image Resource Blocks". + The keys are the image resource ID. For more information, see + https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727 + + :returns: Photoshop "Image Resource Blocks" in a dictionary. + """ + blocks = {} + val = self.tag_v2.get(0x8649) + if val: + while val[:4] == b"8BIM": + id = i16(val[4:6]) + n = math.ceil((val[6] + 1) / 2) * 2 + size = i32(val[6 + n : 10 + n]) + data = val[10 + n : 10 + n + size] + blocks[id] = {"data": data} + + val = val[math.ceil((10 + n + size) / 2) * 2 :] + return blocks + def load(self): if self.tile and self.use_load_libtiff: return self._load_libtiff() @@ -1136,13 +1161,13 @@ class TiffImageFile(ImageFile.ImageFile): def load_end(self): if self._tile_orientation: method = { - 2: Image.FLIP_LEFT_RIGHT, - 3: Image.ROTATE_180, - 4: Image.FLIP_TOP_BOTTOM, - 5: Image.TRANSPOSE, - 6: Image.ROTATE_270, - 7: Image.TRANSVERSE, - 8: Image.ROTATE_90, + 2: Image.Transpose.FLIP_LEFT_RIGHT, + 3: Image.Transpose.ROTATE_180, + 4: Image.Transpose.FLIP_TOP_BOTTOM, + 5: Image.Transpose.TRANSPOSE, + 6: Image.Transpose.ROTATE_270, + 7: Image.Transpose.TRANSVERSE, + 8: Image.Transpose.ROTATE_90, }.get(self._tile_orientation) if method is not None: self.im = self.im.transpose(method) @@ -1307,9 +1332,14 @@ class TiffImageFile(ImageFile.ImageFile): else: bps_count = 1 bps_count += len(extra_tuple) - # Some files have only one value in bps_tuple, - # while should have more. Fix it - if bps_count > len(bps_tuple) and len(bps_tuple) == 1: + bps_actual_count = len(bps_tuple) + if bps_count < bps_actual_count: + # If a file has more values in bps_tuple than expected, + # remove the excess. + bps_tuple = bps_tuple[:bps_count] + elif bps_count > bps_actual_count and bps_actual_count == 1: + # If a file has only one value in bps_tuple, when it should have more, + # presume it is the same number of bits for all of the samples. bps_tuple = bps_tuple * bps_count samplesPerPixel = self.tag_v2.get( diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 45fef241e..e65b155b2 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -30,6 +30,7 @@ _plugins = [ "DcxImagePlugin", "DdsImagePlugin", "EpsImagePlugin", + "FitsImagePlugin", "FitsStubImagePlugin", "FliImagePlugin", "FpxImagePlugin", diff --git a/src/_imaging.c b/src/_imaging.c index 2a42c0461..0888188fb 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1063,7 +1063,7 @@ _gaussian_blur(ImagingObject *self, PyObject *args) { static PyObject * _getpalette(ImagingObject *self, PyObject *args) { PyObject *palette; - int palettesize = 256; + int palettesize; int bits; ImagingShuffler pack; @@ -1084,6 +1084,7 @@ _getpalette(ImagingObject *self, PyObject *args) { return NULL; } + palettesize = self->image->palette->size; palette = PyBytes_FromStringAndSize(NULL, palettesize * bits / 8); if (!palette) { return NULL; @@ -1641,7 +1642,7 @@ _putpalette(ImagingObject *self, PyObject *args) { ImagingShuffler unpack; int bits; - char *rawmode; + char *rawmode, *palette_mode; UINT8 *palette; Py_ssize_t palettesize; if (!PyArg_ParseTuple(args, "sy#", &rawmode, &palette, &palettesize)) { @@ -1654,7 +1655,8 @@ _putpalette(ImagingObject *self, PyObject *args) { return NULL; } - unpack = ImagingFindUnpacker("RGB", rawmode, &bits); + palette_mode = strncmp("RGBA", rawmode, 4) == 0 ? "RGBA" : "RGB"; + unpack = ImagingFindUnpacker(palette_mode, rawmode, &bits); if (!unpack) { PyErr_SetString(PyExc_ValueError, wrong_raw_mode); return NULL; @@ -1669,11 +1671,13 @@ _putpalette(ImagingObject *self, PyObject *args) { strcpy(self->image->mode, strlen(self->image->mode) == 2 ? "PA" : "P"); - self->image->palette = ImagingPaletteNew("RGB"); + self->image->palette = ImagingPaletteNew(palette_mode); - unpack(self->image->palette->palette, palette, palettesize * 8 / bits); + self->image->palette->size = palettesize * 8 / bits; + unpack(self->image->palette->palette, palette, self->image->palette->size); - return PyLong_FromLong(palettesize * 8 / bits); + Py_INCREF(Py_None); + return Py_None; } static PyObject * diff --git a/src/encode.c b/src/encode.c index 2ecf9723b..a52d48f62 100644 --- a/src/encode.c +++ b/src/encode.c @@ -149,14 +149,13 @@ _encode(ImagingEncoderObject *encoder, PyObject *args) { } static PyObject * -_encode_to_pyfd(ImagingEncoderObject *encoder, PyObject *args) { +_encode_to_pyfd(ImagingEncoderObject *encoder) { PyObject *result; int status; if (!encoder->pushes_fd) { // UNDONE, appropriate errcode??? result = Py_BuildValue("ii", 0, IMAGING_CODEC_CONFIG); - ; return result; } @@ -307,7 +306,7 @@ static struct PyMethodDef methods[] = { {"encode", (PyCFunction)_encode, METH_VARARGS}, {"cleanup", (PyCFunction)_encode_cleanup, METH_VARARGS}, {"encode_to_file", (PyCFunction)_encode_to_file, METH_VARARGS}, - {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_VARARGS}, + {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_NOARGS}, {"setimage", (PyCFunction)_setimage, METH_VARARGS}, {"setfd", (PyCFunction)_setfd, METH_VARARGS}, {NULL, NULL} /* sentinel */ diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 517a4dbe3..0f200af6b 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -991,115 +991,116 @@ static struct { /* ------------------- */ static void -p2bit(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +p2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; /* FIXME: precalculate greyscale palette? */ for (x = 0; x < xsize; x++) { - *out++ = (L(&palette[in[x] * 4]) >= 128000) ? 255 : 0; + *out++ = (L(&palette->palette[in[x] * 4]) >= 128000) ? 255 : 0; } } static void -pa2bit(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +pa2bit(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; /* FIXME: precalculate greyscale palette? */ for (x = 0; x < xsize; x++, in += 4) { - *out++ = (L(&palette[in[0] * 4]) >= 128000) ? 255 : 0; + *out++ = (L(&palette->palette[in[0] * 4]) >= 128000) ? 255 : 0; } } static void -p2l(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +p2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; /* FIXME: precalculate greyscale palette? */ for (x = 0; x < xsize; x++) { - *out++ = L24(&palette[in[x] * 4]) >> 16; + *out++ = L24(&palette->palette[in[x] * 4]) >> 16; } } static void -pa2l(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +pa2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; /* FIXME: precalculate greyscale palette? */ for (x = 0; x < xsize; x++, in += 4) { - *out++ = L24(&palette[in[0] * 4]) >> 16; + *out++ = L24(&palette->palette[in[0] * 4]) >> 16; } } static void -p2pa(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; + int rgb = strcmp(palette->mode, "RGB"); for (x = 0; x < xsize; x++, in++) { - const UINT8 *rgba = &palette[in[0]]; + const UINT8 *rgba = &palette->palette[in[0]]; *out++ = in[0]; *out++ = in[0]; *out++ = in[0]; - *out++ = rgba[3]; + *out++ = rgb == 0 ? 255 : rgba[3]; } } static void -p2la(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +p2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; /* FIXME: precalculate greyscale palette? */ for (x = 0; x < xsize; x++, out += 4) { - const UINT8 *rgba = &palette[*in++ * 4]; + const UINT8 *rgba = &palette->palette[*in++ * 4]; out[0] = out[1] = out[2] = L24(rgba) >> 16; out[3] = rgba[3]; } } static void -pa2la(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +pa2la(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; /* FIXME: precalculate greyscale palette? */ for (x = 0; x < xsize; x++, in += 4, out += 4) { - out[0] = out[1] = out[2] = L24(&palette[in[0] * 4]) >> 16; + out[0] = out[1] = out[2] = L24(&palette->palette[in[0] * 4]) >> 16; out[3] = in[3]; } } static void -p2i(UINT8 *out_, const UINT8 *in, int xsize, const UINT8 *palette) { +p2i(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { int x; for (x = 0; x < xsize; x++, out_ += 4) { - INT32 v = L24(&palette[in[x] * 4]) >> 16; + INT32 v = L24(&palette->palette[in[x] * 4]) >> 16; memcpy(out_, &v, sizeof(v)); } } static void -pa2i(UINT8 *out_, const UINT8 *in, int xsize, const UINT8 *palette) { +pa2i(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { int x; INT32 *out = (INT32 *)out_; for (x = 0; x < xsize; x++, in += 4) { - *out++ = L24(&palette[in[0] * 4]) >> 16; + *out++ = L24(&palette->palette[in[0] * 4]) >> 16; } } static void -p2f(UINT8 *out_, const UINT8 *in, int xsize, const UINT8 *palette) { +p2f(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { int x; for (x = 0; x < xsize; x++, out_ += 4) { - FLOAT32 v = L(&palette[in[x] * 4]) / 1000.0F; + FLOAT32 v = L(&palette->palette[in[x] * 4]) / 1000.0F; memcpy(out_, &v, sizeof(v)); } } static void -pa2f(UINT8 *out_, const UINT8 *in, int xsize, const UINT8 *palette) { +pa2f(UINT8 *out_, const UINT8 *in, int xsize, ImagingPalette palette) { int x; FLOAT32 *out = (FLOAT32 *)out_; for (x = 0; x < xsize; x++, in += 4) { - *out++ = (float)L(&palette[in[0] * 4]) / 1000.0F; + *out++ = (float)L(&palette->palette[in[0] * 4]) / 1000.0F; } } static void -p2rgb(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +p2rgb(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; for (x = 0; x < xsize; x++) { - const UINT8 *rgb = &palette[*in++ * 4]; + const UINT8 *rgb = &palette->palette[*in++ * 4]; *out++ = rgb[0]; *out++ = rgb[1]; *out++ = rgb[2]; @@ -1108,10 +1109,10 @@ p2rgb(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { } static void -pa2rgb(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +pa2rgb(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; for (x = 0; x < xsize; x++, in += 4) { - const UINT8 *rgb = &palette[in[0] * 4]; + const UINT8 *rgb = &palette->palette[in[0] * 4]; *out++ = rgb[0]; *out++ = rgb[1]; *out++ = rgb[2]; @@ -1120,30 +1121,30 @@ pa2rgb(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { } static void -p2hsv(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +p2hsv(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; for (x = 0; x < xsize; x++, out += 4) { - const UINT8 *rgb = &palette[*in++ * 4]; + const UINT8 *rgb = &palette->palette[*in++ * 4]; rgb2hsv_row(out, rgb); out[3] = 255; } } static void -pa2hsv(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +pa2hsv(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; for (x = 0; x < xsize; x++, in += 4, out += 4) { - const UINT8 *rgb = &palette[in[0] * 4]; + const UINT8 *rgb = &palette->palette[in[0] * 4]; rgb2hsv_row(out, rgb); out[3] = 255; } } static void -p2rgba(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +p2rgba(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; for (x = 0; x < xsize; x++) { - const UINT8 *rgba = &palette[*in++ * 4]; + const UINT8 *rgba = &palette->palette[*in++ * 4]; *out++ = rgba[0]; *out++ = rgba[1]; *out++ = rgba[2]; @@ -1152,10 +1153,10 @@ p2rgba(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { } static void -pa2rgba(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +pa2rgba(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { int x; for (x = 0; x < xsize; x++, in += 4) { - const UINT8 *rgb = &palette[in[0] * 4]; + const UINT8 *rgb = &palette->palette[in[0] * 4]; *out++ = rgb[0]; *out++ = rgb[1]; *out++ = rgb[2]; @@ -1164,25 +1165,25 @@ pa2rgba(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { } static void -p2cmyk(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +p2cmyk(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { p2rgb(out, in, xsize, palette); rgb2cmyk(out, out, xsize); } static void -pa2cmyk(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +pa2cmyk(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { pa2rgb(out, in, xsize, palette); rgb2cmyk(out, out, xsize); } static void -p2ycbcr(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +p2ycbcr(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { p2rgb(out, in, xsize, palette); ImagingConvertRGB2YCbCr(out, out, xsize); } static void -pa2ycbcr(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) { +pa2ycbcr(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { pa2rgb(out, in, xsize, palette); ImagingConvertRGB2YCbCr(out, out, xsize); } @@ -1192,7 +1193,7 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { ImagingSectionCookie cookie; int alpha; int y; - void (*convert)(UINT8 *, const UINT8 *, int, const UINT8 *); + void (*convert)(UINT8 *, const UINT8 *, int, ImagingPalette); /* Map palette image to L, RGB, RGBA, or CMYK */ @@ -1216,9 +1217,7 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { convert = alpha ? pa2f : p2f; } else if (strcmp(mode, "RGB") == 0) { convert = alpha ? pa2rgb : p2rgb; - } else if (strcmp(mode, "RGBA") == 0) { - convert = alpha ? pa2rgba : p2rgba; - } else if (strcmp(mode, "RGBX") == 0) { + } else if (strcmp(mode, "RGBA") == 0 || strcmp(mode, "RGBX") == 0) { convert = alpha ? pa2rgba : p2rgba; } else if (strcmp(mode, "CMYK") == 0) { convert = alpha ? pa2cmyk : p2cmyk; @@ -1241,7 +1240,7 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) { (UINT8 *)imOut->image[y], (UINT8 *)imIn->image[y], imIn->xsize, - imIn->palette->palette); + imIn->palette); } ImagingSectionLeave(&cookie); @@ -1448,7 +1447,7 @@ topalette( } static Imaging -tobilevel(Imaging imOut, Imaging imIn, int dither) { +tobilevel(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y; int *errors; @@ -1575,7 +1574,7 @@ convert( } if (dither && strcmp(mode, "1") == 0) { - return tobilevel(imOut, imIn, dither); + return tobilevel(imOut, imIn); } /* standard conversion machinery */ diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 9b1c1024d..b65f8eadd 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -143,6 +143,7 @@ struct ImagingPaletteInstance { char mode[IMAGING_MODE_LENGTH]; /* Band names */ /* Data */ + int size; UINT8 palette[1024]; /* Palette data (same format as image data) */ INT16 *cache; /* Palette cache (used for predefined palettes) */ diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 0c7c0497e..01760e742 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -574,6 +574,7 @@ static struct { /* true colour */ {"RGB", "RGB", 24, ImagingPackRGB}, {"RGB", "RGBX", 32, copy4}, + {"RGB", "RGBA", 32, copy4}, {"RGB", "XRGB", 32, ImagingPackXRGB}, {"RGB", "BGR", 24, ImagingPackBGR}, {"RGB", "BGRX", 32, ImagingPackBGRX}, diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c index 43bea61e3..20c6bc84b 100644 --- a/src/libImaging/Palette.c +++ b/src/libImaging/Palette.c @@ -40,6 +40,7 @@ ImagingPaletteNew(const char *mode) { palette->mode[IMAGING_MODE_LENGTH - 1] = 0; /* Initialize to ramp */ + palette->size = 256; for (i = 0; i < 256; i++) { palette->palette[i * 4 + 0] = palette->palette[i * 4 + 1] = palette->palette[i * 4 + 2] = (UINT8)i; @@ -193,7 +194,7 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) { dmax = (unsigned int)~0; - for (i = 0; i < 256; i++) { + for (i = 0; i < palette->size; i++) { int r, g, b; unsigned int tmin, tmax; @@ -226,7 +227,7 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) { d[i] = (unsigned int)~0; } - for (i = 0; i < 256; i++) { + for (i = 0; i < palette->size; i++) { if (dmin[i] <= dmax) { int rd, gd, bd; int ri, gi, bi; diff --git a/src/libImaging/TgaRleDecode.c b/src/libImaging/TgaRleDecode.c index 273ecdffd..77248d145 100644 --- a/src/libImaging/TgaRleDecode.c +++ b/src/libImaging/TgaRleDecode.c @@ -20,6 +20,8 @@ int ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { int n, depth; UINT8 *ptr; + UINT8 extra_data = 0; + int extra_bytes = 0; ptr = buf; @@ -42,15 +44,13 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t return ptr - buf; } + n = depth * ((ptr[0] & 0x7f) + 1); if (ptr[0] & 0x80) { /* Run (1 + pixelsize bytes) */ - if (bytes < 1 + depth) { break; } - n = depth * ((ptr[0] & 0x7f) + 1); - if (state->x + n > state->bytes) { state->errcode = IMAGING_CODEC_OVERRUN; return -1; @@ -67,18 +67,17 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t ptr += 1 + depth; bytes -= 1 + depth; - } else { /* Literal (1+n+1 bytes block) */ - n = depth * (ptr[0] + 1); - if (bytes < 1 + n) { break; } if (state->x + n > state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; + extra_bytes = n; /* full value */ + n = state->bytes - state->x; + extra_bytes -= n; + extra_data = ptr[1]; } memcpy(state->buffer + state->x, ptr + 1, n); @@ -87,24 +86,43 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes -= 1 + n; } - state->x += n; + for (;;) { + state->x += n; - if (state->x >= state->bytes) { - /* Got a full line, unpack it */ - state->shuffle( - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->buffer, - state->xsize); + if (state->x >= state->bytes) { + /* Got a full line, unpack it */ + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer, + state->xsize); - state->x = 0; + state->x = 0; - state->y += state->ystep; + state->y += state->ystep; - if (state->y < 0 || state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; + if (state->y < 0 || state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } } + + if (extra_bytes == 0) { + break; + } + + if (state->x > 0) { + break; // assert + } + + if (extra_bytes >= state->bytes) { + n = state->bytes; + } else { + n = extra_bytes; + } + memcpy(state->buffer + state->x, ptr, n); + ptr += n; + extra_bytes -= n; } } diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 5dac95c1d..4f9838fa8 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1529,6 +1529,7 @@ static struct { {"RGB", "RGBX", 32, copy4}, {"RGB", "RGBX;L", 32, unpackRGBAL}, {"RGB", "RGBA;L", 32, unpackRGBAL}, + {"RGB", "RGBA;15", 16, ImagingUnpackRGBA15}, {"RGB", "BGRX", 32, ImagingUnpackBGRX}, {"RGB", "XRGB", 32, ImagingUnpackXRGB}, {"RGB", "XBGR", 32, ImagingUnpackXBGR}, diff --git a/tox.ini b/tox.ini index bdedc2bd5..09db05884 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,8 @@ envlist = minversion = 1.9 [testenv] +extras = + tests commands = make clean {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . @@ -18,9 +20,6 @@ commands = deps = cffi numpy - olefile - pyroma - pytest [testenv:lint] commands = diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 01f1bac29..3ff1aeca0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -107,9 +107,9 @@ header = [ # dependencies, listed in order of compilation deps = { "libjpeg": { - "url": SF_MIRROR + "/project/libjpeg-turbo/2.1.2/libjpeg-turbo-2.1.2.tar.gz", - "filename": "libjpeg-turbo-2.1.2.tar.gz", - "dir": "libjpeg-turbo-2.1.2", + "url": SF_MIRROR + "/project/libjpeg-turbo/2.1.3/libjpeg-turbo-2.1.3.tar.gz", + "filename": "libjpeg-turbo-2.1.3.tar.gz", + "dir": "libjpeg-turbo-2.1.3", "build": [ cmd_cmake( [ @@ -221,9 +221,9 @@ deps = { # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { - "url": SF_MIRROR + "/project/lcms/lcms/2.13/lcms2-2.13.tar.gz", - "filename": "lcms2-2.13.tar.gz", - "dir": "lcms2-2.13", + "url": SF_MIRROR + "/project/lcms/lcms/2.13/lcms2-2.13.1.tar.gz", + "filename": "lcms2-2.13.1.tar.gz", + "dir": "lcms2-2.13.1", "patch": { r"Projects\VC2019\lcms2_static\lcms2_static.vcxproj": { # default is /MD for x86 and /MT for x64, we need /MD always @@ -280,9 +280,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/3.3.1.zip", - "filename": "harfbuzz-3.3.1.zip", - "dir": "harfbuzz-3.3.1", + "url": "https://github.com/harfbuzz/harfbuzz/archive/4.0.0.zip", + "filename": "harfbuzz-4.0.0.zip", + "dir": "harfbuzz-4.0.0", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"),