diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 7e2fbf28f..0e0abaf95 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -31,13 +31,13 @@ jobs: language: python dry-run: false - name: Upload New Crash - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() && steps.build.outcome == 'success' with: name: artifacts path: ./out/artifacts - name: Upload Legacy Crash - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: steps.run.outcome == 'success' with: name: crash diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 533ce8cbd..4540fb5af 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: name: Lint steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: pre-commit cache uses: actions/cache@v2 @@ -21,7 +21,7 @@ jobs: lint-pre-commit- - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: "3.10" cache: pip diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index db866774c..f583eae10 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -25,6 +25,7 @@ jobs: debian-11-bullseye-x86, fedora-34-amd64, fedora-35-amd64, + gentoo, ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, ] @@ -40,7 +41,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build system information run: python3 .github/workflows/system-info.py diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 8a9c1725d..7b5cc8a97 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up shell run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH @@ -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-valgrind.yml b/.github/workflows/test-valgrind.yml index 4a8966ca8..21a2b469e 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -28,7 +28,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build system information run: python3 .github/workflows/system-info.py diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index c78f9fd24..64289cc3a 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -23,17 +23,17 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Checkout cached dependencies - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: python-pillow/pillow-depends path: winbuild\depends # sets env: pythonLocation - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.architecture }} @@ -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: | @@ -155,7 +156,7 @@ jobs: shell: bash - name: Upload errors - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() with: name: errors @@ -181,7 +182,7 @@ jobs: winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel shell: cmd - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 if: "github.event_name != 'pull_request'" with: name: ${{ steps.wheel.outputs.dist }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 414c7e94e..fef442cfd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,10 +36,10 @@ jobs: name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} cache: pip @@ -84,14 +84,14 @@ jobs: mkdir -p Tests/errors - name: Upload errors - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() with: name: errors 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/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml index c2b8b3bda..2e8c9b730 100644 --- a/.github/workflows/tidelift.yml +++ b/.github/workflows/tidelift.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Scan uses: tidelift/alignment-action@main env: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 822fa43ca..b54650565 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: f1d4e742c91dd5179d742b0db9293c4472b765f8 # frozen: 21.12b0 + rev: fc0be6eb1e2a96091e6f64009ee5e9081bf8b6c6 # frozen: 22.1.0 hooks: - id: black args: ["--target-version", "py37"] @@ -19,7 +19,7 @@ repos: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10 + rev: ca52c4245639abd55c970e6bbbca95cab3de22d8 # frozen: v1.1.13 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) 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 ca1dacba4..6662b5cd5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,27 @@ Changelog (Pillow) 9.1.0 (unreleased) ------------------ +- When converting, clip I;16 to be unsigned, not signed #6112 + [radarhere] + +- Fixed loading L mode GIF with transparency #6086 + [radarhere] + +- Improved handling of PPM header #5121 + [Piolie, radarhere] + +- Reset size when seeking away from "Large Thumbnail" MPO frame #6101 + [radarhere] + +- Replace requirements.txt with extras #6072 + [hugovk, radarhere] + +- Added PyEncoder and support BLP saving #6069 + [radarhere] + +- Handle TGA images with packets that cross scan lines #6087 + [radarhere] + - Added FITS reading #6056 [radarhere, hugovk] 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/32bit_segfault_check.py b/Tests/32bit_segfault_check.py index e19cdf7a9..2ff7f908f 100755 --- a/Tests/32bit_segfault_check.py +++ b/Tests/32bit_segfault_check.py @@ -4,5 +4,5 @@ import sys from PIL import Image -if sys.maxsize < 2 ** 32: +if sys.maxsize < 2**32: im = Image.new("L", (999999, 999999), 0) diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index c191ffc1e..d98f4a694 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -23,7 +23,7 @@ YDIM = 32769 XDIM = 48000 -pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") def _write_png(tmp_path, xdim, ydim): diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 70ae6d230..24cb1f722 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -19,7 +19,7 @@ YDIM = 32769 XDIM = 48000 -pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") def _write_png(tmp_path, xdim, ydim): 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/no_palette_with_transparency.gif b/Tests/images/no_palette_with_transparency.gif new file mode 100644 index 000000000..3cd1c0c48 Binary files /dev/null and b/Tests/images/no_palette_with_transparency.gif 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_core_resources.py b/Tests/test_core_resources.py index 6c52d25a4..385192a3c 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -110,9 +110,9 @@ class TestCoreMemory: with pytest.raises(ValueError): Image.core.set_blocks_max(-1) - if sys.maxsize < 2 ** 32: + if sys.maxsize < 2**32: with pytest.raises(ValueError): - Image.core.set_blocks_max(2 ** 29) + Image.core.set_blocks_max(2**29) @pytest.mark.skipif(is_pypy(), reason="Images not collected") def test_set_blocks_max_stats(self): diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index c2f8d08cb..c1fae44ca 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -2,7 +2,12 @@ import pytest 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(): @@ -25,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", [ 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_dds.py b/Tests/test_file_dds.py index 2f46ed77e..58447122e 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -196,6 +196,13 @@ def test__accept_false(): assert not output +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + DdsImagePlugin.DdsImageFile(invalid_file) + + def test_short_header(): """Check a short header""" with open(TEST_FILE_DXT5, "rb") as f: 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 5447dc740..cae20fa46 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -16,6 +16,13 @@ def test_load_dxt1(): assert_image_similar(im, target.convert("RGBA"), 15) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + FtexImagePlugin.FtexImageFile(invalid_file) + + def test_constants_deprecation(): for enum, prefix in { FtexImagePlugin.Format: "FORMAT_", diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 8b0306db8..924adad9e 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" @@ -62,6 +59,17 @@ def test_invalid_file(): GifImagePlugin.GifImageFile(invalid_file) +def test_l_mode_transparency(): + with Image.open("Tests/images/no_palette_with_transparency.gif") as im: + assert im.mode == "L" + assert im.load()[0, 0] == 0 + assert im.info["transparency"] == 255 + + im.seek(1) + assert im.mode == "LA" + assert im.load()[0, 0] == (0, 255) + + def test_optimize(): def test_grayscale(optimize): im = Image.new("L", (1, 1), 0) @@ -311,6 +319,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 diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index a22fe0400..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) 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 f0cfb7811..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 @@ -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_libtiff.py b/Tests/test_file_libtiff.py index 53ed2520a..a9337f4fc 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -218,7 +218,7 @@ class TestFileLibTiff(LibTiffTestCase): values = { 2: "test", 3: 1, - 4: 2 ** 20, + 4: 2**20, 5: TiffImagePlugin.IFDRational(100, 1), 12: 1.05, } @@ -1019,7 +1019,7 @@ class TestFileLibTiff(LibTiffTestCase): im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") - TiffImagePlugin.STRIP_SIZE = 2 ** 18 + TiffImagePlugin.STRIP_SIZE = 2**18 try: im.save(out, compression="tiff_adobe_deflate") 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 fc4fc2a1b..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] diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 2213af5aa..056295516 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -258,7 +258,7 @@ def test_ifd_unsigned_rational(tmp_path): im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() - max_long = 2 ** 32 - 1 + max_long = 2**32 - 1 # 4 bytes unsigned long numerator = max_long @@ -290,8 +290,8 @@ def test_ifd_signed_rational(tmp_path): info = TiffImagePlugin.ImageFileDirectory_v2() # pair of 4 byte signed longs - numerator = 2 ** 31 - 1 - denominator = -(2 ** 31) + numerator = 2**31 - 1 + denominator = -(2**31) info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) @@ -302,8 +302,8 @@ def test_ifd_signed_rational(tmp_path): assert numerator == reloaded.tag_v2[37380].numerator assert denominator == reloaded.tag_v2[37380].denominator - numerator = -(2 ** 31) - denominator = 2 ** 31 - 1 + numerator = -(2**31) + denominator = 2**31 - 1 info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) @@ -315,7 +315,7 @@ def test_ifd_signed_rational(tmp_path): assert denominator == reloaded.tag_v2[37380].denominator # out of bounds of 4 byte signed long - numerator = -(2 ** 31) - 1 + numerator = -(2**31) - 1 denominator = 1 info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) @@ -324,7 +324,7 @@ def test_ifd_signed_rational(tmp_path): im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: - assert 2 ** 31 - 1 == reloaded.tag_v2[37380].numerator + assert 2**31 - 1 == reloaded.tag_v2[37380].numerator assert -1 == reloaded.tag_v2[37380].denominator diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index e72b4993c..051119378 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 @@ -127,7 +128,7 @@ class TestFileWebp: self._roundtrip(tmp_path, "P", 50.0) - @pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_message(self, tmp_path): temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) @@ -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_file_xbm.py b/Tests/test_file_xbm.py index 487920a92..9c54c6755 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -2,7 +2,7 @@ from io import BytesIO import pytest -from PIL import Image +from PIL import Image, XbmImagePlugin from .helper import hopper @@ -63,6 +63,13 @@ def test_open_filename_with_underscore(): assert im.size == (128, 128) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + XbmImagePlugin.XbmImageFile(invalid_file) + + def test_save_wrong_mode(tmp_path): im = hopper() out = str(tmp_path / "temp.xbm") diff --git a/Tests/test_image.py b/Tests/test_image.py index b616d06ff..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: diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 7b3036979..5bca08582 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -154,14 +154,17 @@ class TestImageGetPixel(AccessTest): # Check 0 im = Image.new(mode, (0, 0), None) - with pytest.raises(IndexError): + assert im.load() is not None + + error = ValueError if self._need_cffi_access else IndexError + with pytest.raises(error): im.putpixel((0, 0), c) - with pytest.raises(IndexError): + with pytest.raises(error): im.getpixel((0, 0)) # Check 0 negative index - with pytest.raises(IndexError): + with pytest.raises(error): im.putpixel((-1, -1), c) - with pytest.raises(IndexError): + with pytest.raises(error): im.getpixel((-1, -1)) # check initial color @@ -176,10 +179,10 @@ class TestImageGetPixel(AccessTest): # Check 0 im = Image.new(mode, (0, 0), c) - with pytest.raises(IndexError): + with pytest.raises(error): im.getpixel((0, 0)) # Check 0 negative index - with pytest.raises(IndexError): + with pytest.raises(error): im.getpixel((-1, -1)) def test_basic(self): @@ -205,10 +208,10 @@ class TestImageGetPixel(AccessTest): # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* for mode in ("I;16", "I;16B"): - self.check(mode, 2 ** 15 - 1) - self.check(mode, 2 ** 15) - self.check(mode, 2 ** 15 + 1) - self.check(mode, 2 ** 16 - 1) + self.check(mode, 2**15 - 1) + self.check(mode, 2**15) + self.check(mode, 2**15 + 1) + self.check(mode, 2**16 - 1) def test_p_putpixel_rgb_rgba(self): for color in [(255, 0, 0), (255, 0, 0, 255)]: @@ -386,7 +389,7 @@ class TestImagePutPixelError(AccessTest): def test_putpixel_overflow_error(self, mode): im = hopper(mode) with pytest.raises(OverflowError): - im.putpixel((0, 0), 2 ** 80) + im.putpixel((0, 0), 2**80) def test_putpixel_unrecognized_mode(self): im = hopper("BGR;15") diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 1d6469819..727c282d7 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -70,6 +70,11 @@ def test_16bit(): with Image.open("Tests/images/16bit.cropped.tif") as im: _test_float_conversion(im) + for color in (65535, 65536): + im = Image.new("I", (1, 1), color) + im_i16 = im.convert("I;16") + assert im_i16.getpixel((0, 0)) == 65535 + def test_16bit_workaround(): with Image.open("Tests/images/16bit.cropped.tif") as im: @@ -135,6 +140,10 @@ def test_trns_l(tmp_path): f = str(tmp_path / "temp.png") + im_la = im.convert("LA") + assert "transparency" not in im_la.info + im_la.save(f) + im_rgb = im.convert("RGB") assert im_rgb.info["transparency"] == (128, 128, 128) # undone im_rgb.save(f) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index dc3caef01..281d5a6fb 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -67,6 +67,16 @@ class TestImagingPaste: ], ) + @cached_property + def gradient_LA(self): + return Image.merge( + "LA", + [ + self.gradient_L, + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + ], + ) + @cached_property def gradient_RGBA(self): return Image.merge( @@ -145,6 +155,28 @@ class TestImagingPaste: ], ) + def test_image_mask_LA(self): + for mode in ("RGBA", "RGB", "L"): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.gradient_LA, + [ + (128, 191, 255, 191), + (112, 207, 206, 111), + (128, 254, 128, 1), + (208, 208, 239, 239), + (192, 191, 191, 191), + (207, 207, 112, 113), + (255, 255, 255, 255), + (239, 207, 207, 239), + (255, 191, 128, 191), + ], + ) + def test_image_mask_RGBA(self): for mode in ("RGBA", "RGB", "L"): im = Image.new(mode, (200, 200), "white") diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 7e4bbaaec..3d60e52a2 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -38,7 +38,7 @@ def test_long_integers(): assert put(0xFFFFFFFF) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) - if sys.maxsize > 2 ** 32: + if sys.maxsize > 2**32: assert put(sys.maxsize) == (255, 255, 255, 255) else: assert put(sys.maxsize) == (255, 255, 255, 127) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index e0093739c..66a72a90e 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -303,7 +303,7 @@ def test_extended_information(): def assert_truncated_tuple_equal(tup1, tup2, digits=10): # Helper function to reduce precision of tuples of floats # recursively and then check equality. - power = 10 ** digits + power = 10**digits def truncate_tuple(tuple_or_float): return tuple( diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 3e86477c5..1c444fe27 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -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,14 @@ class MockPyDecoder(ImageFile.PyDecoder): return -1, 0 +class MockPyEncoder(ImageFile.PyEncoder): + def encode(self, buffer): + return 1, 1, b"" + + def cleanup(self): + self.cleanup_called = True + + xoff, yoff, xsize, ysize = 10, 20, 100, 100 @@ -190,53 +215,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 +280,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 +288,92 @@ 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() + self.encoder.cleanup_called = False + with pytest.raises(ValueError): + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] + ) + assert self.encoder.cleanup_called + + with pytest.raises(ValueError): + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] + ) + + def test_oversize(self): + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], + ) + + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], + ) + + def test_encode(self): + encoder = ImageFile.PyEncoder(None) + with pytest.raises(NotImplementedError): + encoder.encode(None) + + bytes_consumed, errcode = encoder.encode_to_pyfd() + assert bytes_consumed == 0 + assert ImageFile.ERRORS[errcode] == "bad configuration" + + encoder._pushes_fd = True + with pytest.raises(NotImplementedError): + encoder.encode_to_pyfd() + + with pytest.raises(NotImplementedError): + encoder.encode_to_file(None, None) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 177b18202..f9d0a4c4f 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -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) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 589cb5a21..a42240d49 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import ImageQt @@ -30,10 +32,10 @@ def test_rgb(): def checkrgb(r, g, b): val = ImageQt.rgb(r, g, b) - val = val % 2 ** 24 # drop the alpha + val = val % 2**24 # drop the alpha assert val >> 16 == r - assert ((val >> 8) % 2 ** 8) == g - assert val % 2 ** 8 == b + assert ((val >> 8) % 2**8) == g + assert val % 2**8 == b checkrgb(0, 0, 0) checkrgb(255, 0, 0) @@ -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_imagestat.py b/Tests/test_imagestat.py index 9474ff6f9..5717fe150 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -51,8 +51,8 @@ def test_constant(): st = ImageStat.Stat(im) assert st.extrema[0] == (128, 128) - assert st.sum[0] == 128 ** 3 - assert st.sum2[0] == 128 ** 4 + assert st.sum[0] == 128**3 + assert st.sum2[0] == 128**4 assert st.mean[0] == 128 assert st.median[0] == 128 assert st.rms[0] == 128 diff --git a/Tests/test_map.py b/Tests/test_map.py index 42f3447eb..d816bddaf 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -36,7 +36,7 @@ def test_tobytes(): Image.MAX_IMAGE_PIXELS = max_pixels -@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") +@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_ysize(): numpy = pytest.importorskip("numpy", reason="NumPy not installed") 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/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index 2d428e95f..ea9b33dfc 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -115,6 +115,6 @@ def test_pdf_repr(): assert pdf_repr(True) == b"true" assert pdf_repr(False) == b"false" assert pdf_repr(None) == b"null" - assert pdf_repr(b"a)/b\\(c") == br"(a\)/b\\\(c)" + assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)" assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" 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/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 272409416..29fefba16 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -210,7 +210,9 @@ class DdsImageFile(ImageFile.ImageFile): format_description = "DirectDraw Surface" def _open(self): - magic, header_size = struct.unpack("= 2.1.0 no longer automatically imports any file in the Python path with a name ending in @@ -124,8 +123,12 @@ The ``tile`` attribute To be able to read the file as well as just identifying it, the ``tile`` attribute must also be set. This attribute consists of a list of tile descriptors, where each descriptor specifies how data should be loaded to a -given region in the image. In most cases, only a single descriptor is used, -covering the full image. +given region in the image. + +In most cases, only a single descriptor is used, covering the full image. +:py:class:`.PsdImagePlugin.PsdImageFile` uses multiple tiles to combine +channels within a single layer, given that the channels are stored separately, +one after the other. The tile descriptor is a 4-tuple with the following contents:: @@ -325,42 +328,42 @@ The fields are used as follows: Whether the first line in the image is the top line on the screen (1), or the bottom line (-1). If omitted, the orientation defaults to 1. -.. _file-decoders: +.. _file-codecs: -Writing Your Own File Decoder in C -================================== +Writing Your Own File Codec in C +================================ -There are 3 stages in a file decoder's lifetime: +There are 3 stages in a file codec's lifetime: -1. Setup: Pillow looks for a function in the decoder registry, falling - back to a function named ``[decodername]_decoder`` on the internal - core image object. That function is called with the ``args`` tuple - from the ``tile`` setup in the ``_open`` method. +1. Setup: Pillow looks for a function in the decoder or encoder registry, + falling back to a function named ``[codecname]_decoder`` or + ``[codecname]_encoder`` on the internal core image object. That function is + called with the ``args`` tuple from the ``tile``. -2. Decoding: The decoder's decode function is repeatedly called with - chunks of image data. +2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly + called with chunks of image data. -3. Cleanup: If the decoder has registered a cleanup function, it will - be called at the end of the decoding process, even if there was an +3. Cleanup: If the codec has registered a cleanup function, it will + be called at the end of the transformation process, even if there was an exception raised. Setup ----- -The current conventions are that the decoder setup function is named -``PyImaging_[Decodername]DecoderNew`` and defined in ``decode.c``. The -python binding for it is named ``[decodername]_decoder`` and is setup -from within the ``_imaging.c`` file in the codecs section of the -function array. +The current conventions are that the codec setup function is named +``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew`` +and defined in ``decode.c`` or ``encode.c``. The Python binding for it is +named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from +within the ``_imaging.c`` file in the codecs section of the function array. -The setup function needs to call ``PyImaging_DecoderNew`` and at the -very least, set the ``decode`` function pointer. The fields of -interest in this object are: +The setup function needs to call ``PyImaging_DecoderNew`` or +``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or +``encode`` function pointer. The fields of interest in this object are: -**decode** - Function pointer to the decode function, which has access to - ``im``, ``state``, and the buffer of data to be added to the image. +**decode**/**encode** + Function pointer to the decode or encode function, which has access to + ``im``, ``state``, and the buffer of data to be transformed. **cleanup** Function pointer to the cleanup function, has access to ``state``. @@ -370,36 +373,34 @@ interest in this object are: **state** An ImagingCodecStateInstance, will be set by Pillow. The ``context`` - member is an opaque struct that can be used by the decoder to store + member is an opaque struct that can be used by the codec to store any format specific state or options. -**pulls_fd** - **EXPERIMENTAL** -- **WARNING**, interface may change. If set to 1, - ``state->fd`` will be a pointer to the Python file like object. The - decoder may use the functions in ``codec_fd.c`` to read directly - from the file like object rather than have the data pushed through a - buffer. Note that this implementation may be refactored until this - warning is removed. +**pulls_fd**/**pushes_fd** + If the decoder has ``pulls_fd`` or the encoder has ``pushes_fd`` set to 1, + ``state->fd`` will be a pointer to the Python file like object. The codec may + use the functions in ``codec_fd.c`` to read or write directly with the file + like object rather than have the data pushed through a buffer. .. versionadded:: 3.3.0 -Decoding --------- +Transforming +------------ -The decode function is called with the target (core) image, the -decoder state structure, and a buffer of data to be decoded. +The decode or encode function is called with the target (core) image, the codec +state structure, and a buffer of data to be transformed. -**Experimental** -- If ``pulls_fd`` is set, then the decode function -is called once, with an empty buffer. It is the decoder's -responsibility to decode the entire tile in that one call. The rest of -this section only applies if ``pulls_fd`` is not set. +It is the codec's responsibility to pull as much data as possible out of the +buffer and return the number of bytes consumed. The next call to the codec will +include the previous unconsumed tail. The codec function will be called +multiple times as the data processed. -It is the decoder's responsibility to pull as much data as possible -out of the buffer and return the number of bytes consumed. The next -call to the decoder will include the previous unconsumed tail. The -decoder function will be called multiple times as the data is read -from the file like object. +Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or +encode function is called once, with an empty buffer. It is the codec's +responsibility to transform the entire tile in that one call. Using this will +provide a codec with more freedom, but that freedom may mean increased memory +usage if the entire tile is held in memory at once by the codec. If an error occurs, set ``state->errcode`` and return -1. @@ -408,28 +409,49 @@ Return -1 on success, without setting the errcode. Cleanup ------- -The cleanup function is called after the decoder returns a negative -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. +The cleanup function is called after the codec returns a negative +value, or if there is an error. 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 +1. Setup: Pillow looks for the codec in the decoder or encoder registry, then instantiates the class. -2. Decoding: The decoder instance's ``decode`` method is repeatedly - called with a buffer of data to be interpreted. +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 decoder instance's ``cleanup`` method is called. + Alternatively, if the decoder's ``_pulls_fd`` property (or the encoder's + ``_pushes_fd`` property) is set to ``True``, then ``decode`` and ``encode`` + will only be called once. In the decoder, ``self.fd`` can be used to access + the file-like object. Using this will provide a codec with more freedom, but + that freedom may mean increased memory usage if entire file is held in + memory at once by the codec. + In ``decode``, once the data has been interpreted, ``set_as_raw`` can be + used to populate the image. + +3. Cleanup: The instance's ``cleanup`` method is called once the transformation + is complete. This can be used to clean up any resources used by the codec. + + If you set ``_pulls_fd`` or ``_pushes_fd`` to ``True`` however, then you + probably chose to perform any cleanup tasks at the end of ``decode`` or + ``encode``. + +For an example :py:class:`PIL.ImageFile.PyDecoder`, see `DdsImagePlugin +`_. +For a plugin that uses both :py:class:`PIL.ImageFile.PyDecoder` and +:py:class:`PIL.ImageFile.PyEncoder`, see `BlpImagePlugin +`_ diff --git a/docs/installation.rst b/docs/installation.rst index 4add81352..d59f6b8e4 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -465,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 | 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/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/ImageStat.rst b/docs/reference/ImageStat.rst index 5bb735296..f61d12313 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -14,6 +14,16 @@ for a region of an image. statistics. You can also pass in a previously calculated histogram. :param image: A PIL image, or a precalculated histogram. + + .. note:: + + For a PIL image, calculations rely on the + :py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are + grouped into 256 bins, even if the image has more than 8 bits per + channel. So ``I`` and ``F`` mode images have a maximum ``mean``, + ``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum + of more than 255. + :param mask: An optional mask. .. py:attribute:: extrema diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 173a0bcc0..d2e80fb8c 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -6,7 +6,13 @@ The PixelAccess class provides read and write access to :py:class:`PIL.Image` data at a pixel level. -.. note:: Accessing individual pixels is fairly slow. If you are looping over all of the pixels in an image, there is likely a faster way using other parts of the Pillow API. +.. note:: Accessing individual pixels is fairly slow. If you are + looping over all of the pixels in an image, there is likely + a faster way using other parts of the Pillow API. + + :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps` + have methods for many standard operations. If you wish to perform + a custom mapping, check out :py:meth:`~PIL.Image.Image.point`. Example ------- @@ -39,7 +45,7 @@ Access using negative indexes is also possible. :py:class:`PixelAccess` Class ------------------------------------ +----------------------------- .. class:: PixelAccess diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index e77944d20..f9eb9b524 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -7,8 +7,12 @@ The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version. .. note:: Accessing individual pixels is fairly slow. If you are - looping over all of the pixels in an image, there is likely - a faster way using other parts of the Pillow API. + looping over all of the pixels in an image, there is likely + a faster way using other parts of the Pillow API. + + :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps` + have methods for many standard operations. If you wish to perform + a custom mapping, check out :py:meth:`~PIL.Image.Image.point`. Example ------- 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/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index f377656b3..22b185e95 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -18,11 +18,37 @@ Rather than returning a ``SystemError``, passing the incorrect types of coordina a path will now raise a more specific ``ValueError``, with the message "incorrect 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. @@ -87,7 +113,7 @@ Deprecated Use instead ===================================================== ============================================================ 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 @@ -98,7 +124,7 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. ``viewer.show_file(path="test.jpg")`` instead. FitsStubImagePlugin -~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^ .. deprecated:: 9.1.0 @@ -127,6 +153,13 @@ By default, :py:meth:`~PIL.Image.Image.getpalette` returns RGB data from the pal A ``rawmode`` argument has been added, to allow the mode to be chosen instead. ``None`` 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 ============= @@ -143,3 +176,10 @@ 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/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/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/setup.py b/setup.py index 23d91a5f2..3468b260d 100755 --- a/setup.py +++ b/setup.py @@ -167,7 +167,7 @@ def _find_library_dirs_ldconfig(): # Assuming GLIBC's ldconfig (with option -p) # Alpine Linux uses musl that can't print cache args = ["/sbin/ldconfig", "-p"] - expr = fr".*\({abi_type}.*\) => (.*)" + expr = rf".*\({abi_type}.*\) => (.*)" env = dict(os.environ) env["LC_ALL"] = "C" env["LANG"] = "C" diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 8fd2b8510..ecd3da5df 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -29,6 +29,7 @@ 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 @@ -266,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 @@ -276,50 +281,51 @@ 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(" 2 ** 32 - 1: + if file_size > 2**32 - 1: raise ValueError("File size is too large for the BMP format") fp.write( b"BM" # file type (magic) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 260924fca..3a04bdb5d 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -111,7 +111,9 @@ class DdsImageFile(ImageFile.ImageFile): format_description = "DirectDraw Surface" def _open(self): - magic, header_size = struct.unpack("= 6 and i16(prefix, 4) in [0xAF11, 0xAF12] + return ( + len(prefix) >= 6 + and i16(prefix, 4) in [0xAF11, 0xAF12] + and i16(prefix, 14) in [0, 3] # flags + ) ## @@ -44,11 +48,7 @@ class FliImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(128) - if not ( - _accept(s) - and i16(s, 14) in [0, 3] # flags - and s[20:22] == b"\x00\x00" # reserved - ): + if not (_accept(s) and s[20:22] == b"\x00\x00"): raise SyntaxError("not an FLI/FLC file") # frames diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 8629dcf64..55d28e1ff 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -94,7 +94,8 @@ class FtexImageFile(ImageFile.ImageFile): format_description = "Texture File Format (IW2:EOC)" def _open(self): - struct.unpack(" 100: raise SyntaxError("bad palette file") diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 069aff96b..fa192f053 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -167,7 +167,7 @@ class IcnsFile: self.dct = dct = {} self.fobj = fobj sig, filesize = nextheader(fobj) - if sig != MAGIC: + if not _accept(sig): raise SyntaxError("not an icns file") i = HEADERSIZE while i < filesize: @@ -287,7 +287,7 @@ class IcnsImageFile(ImageFile.ImageFile): ) px = Image.Image.load(self) - if self.im and self.im.size == self.size: + if self.im is not None and self.im.size == self.size: # Already loaded return px self.load_prepare() diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index b4f84ee20..915e4c928 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -304,7 +304,7 @@ class IcoImageFile(ImageFile.ImageFile): self._size = value def load(self): - if self.im and self.im.size == self.size: + if self.im is not None and self.im.size == self.size: # Already loaded return Image.Image.load(self) im = self.ico.getimage(self.size) diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 1dfc808c4..f7e690b35 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -100,7 +100,7 @@ for i in range(2, 33): # -------------------------------------------------------------------- # Read IM directory -split = re.compile(br"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") +split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") def number(s): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c9265b5ab..e174d3743 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -847,7 +847,7 @@ class Image: :returns: An image access object. :rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess` """ - if self.im and self.palette and self.palette.dirty: + if self.im is not None and self.palette and self.palette.dirty: # realize palette mode, arr = self.palette.getdata() self.im.putpalette(mode, arr) @@ -864,7 +864,7 @@ class Image: self.palette.mode = palette_mode self.palette.palette = self.im.getpalette(palette_mode, palette_mode) - if self.im: + if self.im is not None: if cffi and USE_CFFI_ACCESS: if self.pyaccess: return self.pyaccess @@ -975,7 +975,9 @@ class Image: delete_trns = False # transparency handling if has_transparency: - if self.mode in ("1", "L", "I", "RGB") and mode == "RGBA": + if (self.mode in ("1", "L", "I") and mode in ("LA", "RGBA")) or ( + self.mode == "RGB" and mode == "RGBA" + ): # Use transparent conversion to promote from transparent # color to an alpha channel. new_im = self._new( @@ -1492,11 +1494,12 @@ class Image: def histogram(self, mask=None, extrema=None): """ - Returns a histogram for the image. The histogram is returned as - a list of pixel counts, one for each pixel value in the source - image. If the image has more than one band, the histograms for - all bands are concatenated (for example, the histogram for an - "RGB" image contains 768 values). + Returns a histogram for the image. The histogram is returned as a + list of pixel counts, one for each pixel value in the source + image. Counts are grouped into 256 bins for each band, even if + the image has more than 8 bits per band. If the image has more + than one band, the histograms for all bands are concatenated (for + example, the histogram for an "RGB" image contains 768 values). A bilevel image (mode "1") is treated as a greyscale ("L") image by this method. @@ -1564,8 +1567,8 @@ class Image: also use color strings as supported by the ImageColor module. If a mask is given, this method updates only the regions - indicated by the mask. You can use either "1", "L" or "RGBA" - images (in the latter case, the alpha band is used as mask). + indicated by the mask. You can use either "1", "L", "LA", "RGBA" + or "RGBa" images (if present, the alpha band is used as mask). Where the mask is 255, the given image is copied as is. Where the mask is 0, the current value is preserved. Intermediate values will mix the two images together, including their alpha @@ -1613,7 +1616,7 @@ class Image: elif isImageType(im): im.load() if self.mode != im.mode: - if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"): + if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): # should use an adapter for this! im = im.convert(self.mode) im = im.im @@ -2779,9 +2782,9 @@ def frombytes(mode, size, data, decoder_name="raw", *args): In its simplest form, this function takes three arguments (mode, size, and unpacked pixel data). - You can also use any pixel decoder supported by PIL. For more + You can also use any pixel decoder supported by PIL. For more information on available decoders, see the section - :ref:`Writing Your Own File Decoder `. + :ref:`Writing Your Own File Codec `. Note that this function decodes pixel data only, not entire images. If you have an entire image in a string, wrap it in a diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 331410f0e..34f344f1d 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`. +""" # @@ -219,15 +223,15 @@ class ImageFile(Image.Image): ) ] for decoder_name, extents, offset, args in self.tile: + seek(offset) decoder = Image._getdecoder( self.mode, decoder_name, args, self.decoderconfig ) try: - seek(offset) decoder.setimage(self.im, extents) if decoder.pulls_fd: decoder.setfd(self.fp) - status, err_code = decoder.decode(b"") + err_code = decoder.decode(b"")[1] else: b = prefix while True: @@ -495,40 +499,33 @@ def _save(im, fp, tile, bufsize=0): try: fh = fp.fileno() fp.flush() - except (AttributeError, io.UnsupportedOperation) as exc: - # compress to Python file-compatible object - for e, b, o, a in tile: - e = Image._getencoder(im.mode, e, a, im.encoderconfig) - if o > 0: - fp.seek(o) - e.setimage(im.im, b) - if e.pushes_fd: - e.setfd(fp) - l, s = e.encode_to_pyfd() + exc = None + except (AttributeError, io.UnsupportedOperation) as e: + exc = e + for e, b, o, a in tile: + if o > 0: + fp.seek(o) + encoder = Image._getencoder(im.mode, e, a, im.encoderconfig) + try: + encoder.setimage(im.im, b) + if encoder.pushes_fd: + encoder.setfd(fp) + l, s = encoder.encode_to_pyfd() else: - while True: - l, s, d = e.encode(bufsize) - fp.write(d) - if s: - break + if exc: + # compress to Python file-compatible object + while True: + l, s, d = encoder.encode(bufsize) + fp.write(d) + if s: + break + else: + # slight speedup: compress to real file object + s = encoder.encode_to_file(fh, bufsize) if s < 0: raise OSError(f"encoder error {s} when writing image file") from exc - e.cleanup() - else: - # slight speedup: compress to real file object - for e, b, o, a in tile: - e = Image._getencoder(im.mode, e, a, im.encoderconfig) - if o > 0: - fp.seek(o) - e.setimage(im.im, b) - if e.pushes_fd: - e.setfd(fp) - l, s = e.encode_to_pyfd() - else: - s = e.encode_to_file(fh, bufsize) - if s < 0: - raise OSError(f"encoder error {s} when writing image file") - e.cleanup() + finally: + encoder.cleanup() if hasattr(fp, "flush"): fp.flush() @@ -577,16 +574,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 +584,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 +601,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 +643,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 -1 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 +689,60 @@ 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): + """ + If ``pushes_fd`` is ``True``, then this method will be used, + and ``encode()`` will only be called once. + + :returns: A tuple of ``(bytes consumed, errcode)``. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + if not self.pushes_fd: + return 0, -8 # bad configuration + bytes_consumed, errcode, data = self.encode(0) + if data: + self.fd.write(data) + return bytes_consumed, errcode + + def encode_to_file(self, fh, bufsize): + """ + :param fh: File handle. + :param bufsize: Buffer size. + + :returns: If finished successfully, return 0. + Otherwise, return an error code. Err codes are from + :data:`.ImageFile.ERRORS`. + """ + errcode = 0 + while errcode == 0: + status, errcode, buf = self.encode(bufsize) + if status > 0: + fh.write(buf[status:]) + return errcode diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 79e7c4b9f..f21b6de71 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -839,7 +839,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): :file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on 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 diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index f79206921..6109f0bcf 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -25,7 +25,12 @@ _viewers = [] def register(viewer, order=1): """ - The :py:func:`register` function is used to register additional viewers. + The :py:func:`register` function is used to register additional viewers:: + + from PIL import ImageShow + ImageShow.register(MyViewer()) # MyViewer will be used as a last resort + ImageShow.register(MySecondViewer(), 0) # MySecondViewer will be prioritised + ImageShow.register(ImageShow.XVViewer(), 0) # XVViewer will be prioritised :param viewer: The viewer to be registered. :param order: diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index 50bafc972..ef4a1d633 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -91,7 +91,7 @@ class Stat: for i in range(0, len(self.h), 256): sum2 = 0.0 for j in range(256): - sum2 += (j ** 2) * float(self.h[i + j]) + sum2 += (j**2) * float(self.h[i + j]) v.append(sum2) return v diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 21ffd7475..5790acdaf 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -22,7 +22,7 @@ from . import Image, ImageFile # # -------------------------------------------------------------------- -field = re.compile(br"([a-z]*) ([^ \r\n]*)") +field = re.compile(rb"([a-z]*) ([^ \r\n]*)") ## diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index cc7980278..4f4ee8f55 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -132,7 +132,7 @@ def _res_to_dpi(num, denom, exp): calculated as (num / denom) * 10^exp and stored in dots per meter, to floating-point dots per inch.""" if denom != 0: - return (254 * num * (10 ** exp)) / (10000 * denom) + return (254 * num * (10**exp)) / (10000 * denom) def _parse_jp2_header(fp): diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 7ccf27c42..88c1bfcc5 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -46,6 +46,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): self._after_jpeg_open() def _after_jpeg_open(self, mpheader=None): + self._initial_size = self.size self.mpinfo = mpheader if mpheader is not None else self._getmp() self.n_frames = self.mpinfo[0xB001] self.__mpoffsets = [ @@ -77,6 +78,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): segment = self.fp.read(2) if not segment: raise ValueError("No data found for frame") + self._size = self._initial_size if i16(segment) == 0xFFE1: # APP1 n = i16(self.fp.read(2)) - 2 self.info["exif"] = ImageFile._safe_read(self.fp, n) diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 32b28d44d..c4d7ddbb4 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -148,7 +148,7 @@ class MspDecoder(ImageFile.PyDecoder): self.set_as_raw(img.getvalue(), ("1", 0, 1)) - return 0, 0 + return -1, 0 Image.register_decoder("MSP", MspDecoder) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 6ac9c7a7c..9aa0fd6fa 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -576,42 +576,42 @@ class PdfParser: self.xref_table[reference.object_id] = (offset, 0) return reference - delimiter = br"[][()<>{}/%]" - delimiter_or_ws = br"[][()<>{}/%\000\011\012\014\015\040]" - whitespace = br"[\000\011\012\014\015\040]" - whitespace_or_hex = br"[\000\011\012\014\015\0400-9a-fA-F]" + delimiter = rb"[][()<>{}/%]" + delimiter_or_ws = rb"[][()<>{}/%\000\011\012\014\015\040]" + whitespace = rb"[\000\011\012\014\015\040]" + whitespace_or_hex = rb"[\000\011\012\014\015\0400-9a-fA-F]" whitespace_optional = whitespace + b"*" whitespace_mandatory = whitespace + b"+" # No "\012" aka "\n" or "\015" aka "\r": - whitespace_optional_no_nl = br"[\000\011\014\040]*" - newline_only = br"[\r\n]+" + whitespace_optional_no_nl = rb"[\000\011\014\040]*" + newline_only = rb"[\r\n]+" newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl re_trailer_end = re.compile( whitespace_mandatory - + br"trailer" + + rb"trailer" + whitespace_optional - + br"\<\<(.*\>\>)" + + rb"\<\<(.*\>\>)" + newline - + br"startxref" + + rb"startxref" + newline - + br"([0-9]+)" + + rb"([0-9]+)" + newline - + br"%%EOF" + + rb"%%EOF" + whitespace_optional - + br"$", + + rb"$", re.DOTALL, ) re_trailer_prev = re.compile( whitespace_optional - + br"trailer" + + rb"trailer" + whitespace_optional - + br"\<\<(.*?\>\>)" + + rb"\<\<(.*?\>\>)" + newline - + br"startxref" + + rb"startxref" + newline - + br"([0-9]+)" + + rb"([0-9]+)" + newline - + br"%%EOF" + + rb"%%EOF" + whitespace_optional, re.DOTALL, ) @@ -655,12 +655,12 @@ class PdfParser: re_whitespace_optional = re.compile(whitespace_optional) re_name = re.compile( whitespace_optional - + br"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" + + rb"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" + delimiter_or_ws - + br")" + + rb")" ) - re_dict_start = re.compile(whitespace_optional + br"\<\<") - re_dict_end = re.compile(whitespace_optional + br"\>\>" + whitespace_optional) + re_dict_start = re.compile(whitespace_optional + rb"\<\<") + re_dict_end = re.compile(whitespace_optional + rb"\>\>" + whitespace_optional) @classmethod def interpret_trailer(cls, trailer_data): @@ -689,7 +689,7 @@ class PdfParser: ) return trailer - re_hashes_in_name = re.compile(br"([^#]*)(#([0-9a-fA-F]{2}))?") + re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?") @classmethod def interpret_name(cls, raw, as_text=False): @@ -704,53 +704,53 @@ class PdfParser: else: return bytes(name) - re_null = re.compile(whitespace_optional + br"null(?=" + delimiter_or_ws + br")") - re_true = re.compile(whitespace_optional + br"true(?=" + delimiter_or_ws + br")") - re_false = re.compile(whitespace_optional + br"false(?=" + delimiter_or_ws + br")") + re_null = re.compile(whitespace_optional + rb"null(?=" + delimiter_or_ws + rb")") + re_true = re.compile(whitespace_optional + rb"true(?=" + delimiter_or_ws + rb")") + re_false = re.compile(whitespace_optional + rb"false(?=" + delimiter_or_ws + rb")") re_int = re.compile( - whitespace_optional + br"([-+]?[0-9]+)(?=" + delimiter_or_ws + br")" + whitespace_optional + rb"([-+]?[0-9]+)(?=" + delimiter_or_ws + rb")" ) re_real = re.compile( whitespace_optional - + br"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" + + rb"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" + delimiter_or_ws - + br")" + + rb")" ) - re_array_start = re.compile(whitespace_optional + br"\[") - re_array_end = re.compile(whitespace_optional + br"]") + re_array_start = re.compile(whitespace_optional + rb"\[") + re_array_end = re.compile(whitespace_optional + rb"]") re_string_hex = re.compile( - whitespace_optional + br"\<(" + whitespace_or_hex + br"*)\>" + whitespace_optional + rb"\<(" + whitespace_or_hex + rb"*)\>" ) - re_string_lit = re.compile(whitespace_optional + br"\(") + re_string_lit = re.compile(whitespace_optional + rb"\(") re_indirect_reference = re.compile( whitespace_optional - + br"([-+]?[0-9]+)" + + rb"([-+]?[0-9]+)" + whitespace_mandatory - + br"([-+]?[0-9]+)" + + rb"([-+]?[0-9]+)" + whitespace_mandatory - + br"R(?=" + + rb"R(?=" + delimiter_or_ws - + br")" + + rb")" ) re_indirect_def_start = re.compile( whitespace_optional - + br"([-+]?[0-9]+)" + + rb"([-+]?[0-9]+)" + whitespace_mandatory - + br"([-+]?[0-9]+)" + + rb"([-+]?[0-9]+)" + whitespace_mandatory - + br"obj(?=" + + rb"obj(?=" + delimiter_or_ws - + br")" + + rb")" ) re_indirect_def_end = re.compile( - whitespace_optional + br"endobj(?=" + delimiter_or_ws + br")" + whitespace_optional + rb"endobj(?=" + delimiter_or_ws + rb")" ) re_comment = re.compile( - br"(" + whitespace_optional + br"%[^\r\n]*" + newline + br")*" + rb"(" + whitespace_optional + rb"%[^\r\n]*" + newline + rb")*" ) - re_stream_start = re.compile(whitespace_optional + br"stream\r?\n") + re_stream_start = re.compile(whitespace_optional + rb"stream\r?\n") re_stream_end = re.compile( - whitespace_optional + br"endstream(?=" + delimiter_or_ws + br")" + whitespace_optional + rb"endstream(?=" + delimiter_or_ws + rb")" ) @classmethod @@ -876,7 +876,7 @@ class PdfParser: raise PdfFormatError("unrecognized object: " + repr(data[offset : offset + 32])) re_lit_str_token = re.compile( - br"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" + rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" ) escaped_chars = { b"n": b"\n", @@ -922,16 +922,16 @@ class PdfParser: offset = m.end() raise PdfFormatError("unfinished literal string") - re_xref_section_start = re.compile(whitespace_optional + br"xref" + newline) + re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline) re_xref_subsection_start = re.compile( whitespace_optional - + br"([0-9]+)" + + rb"([0-9]+)" + whitespace_mandatory - + br"([0-9]+)" + + rb"([0-9]+)" + whitespace_optional + newline_only ) - re_xref_entry = re.compile(br"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)") + re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)") def read_xref_table(self, xref_section_offset): subsection_found = False diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index cd0a3e0e0..53525e22e 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -48,7 +48,7 @@ from ._binary import o32be as o32 logger = logging.getLogger(__name__) -is_cid = re.compile(br"\w\w\w\w").match +is_cid = re.compile(rb"\w\w\w\w").match _MAGIC = b"\211PNG\r\n\032\n" diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index abf4d651d..9e962cac8 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: @@ -126,7 +136,7 @@ def _save(im, fp, filename): elif im.mode == "L": rawmode, head = "L", b"P5" elif im.mode == "I": - if im.getextrema()[1] < 2 ** 16: + if im.getextrema()[1] < 2**16: rawmode, head = "I;16B", b"P5" else: rawmode, head = "I;32B", b"P5" diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 550a333dd..283219579 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -155,14 +155,6 @@ class PsdImageFile(ImageFile.ImageFile): # return layer number (0=image, 1..max=layers) return self.frame - def load_prepare(self): - # create image memory if necessary - if not self.im or self.im.mode != self.mode or self.im.size != self.size: - self.im = Image.core.fill(self.mode, self.size, 0) - # create palette (optional) - if self.mode == "P": - Image.Image.load(self) - def _close__fp(self): try: if self.__fp != self.fp: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 35ff1c1bb..1d9a4881b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -493,7 +493,7 @@ class ImageFileDirectory_v2(MutableMapping): endianness. :param prefix: Override the endianness of the file. """ - if ifh[:4] not in PREFIXES: + if not _accept(ifh): raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)") self._prefix = prefix if prefix is not None else ifh[:2] if self._prefix == MM: @@ -577,9 +577,9 @@ class ImageFileDirectory_v2(MutableMapping): else TiffTags.SIGNED_RATIONAL ) elif all(isinstance(v, int) for v in values): - if all(0 <= v < 2 ** 16 for v in values): + if all(0 <= v < 2**16 for v in values): self.tagtype[tag] = TiffTags.SHORT - elif all(-(2 ** 15) < v < 2 ** 15 for v in values): + elif all(-(2**15) < v < 2**15 for v in values): self.tagtype[tag] = TiffTags.SIGNED_SHORT else: self.tagtype[tag] = ( @@ -734,7 +734,7 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(5) def write_rational(self, *values): return b"".join( - self._pack("2L", *_limit_rational(frac, 2 ** 32 - 1)) for frac in values + self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values ) @_register_loader(7, 1) @@ -757,7 +757,7 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(10) def write_signed_rational(self, *values): return b"".join( - self._pack("2l", *_limit_signed_rational(frac, 2 ** 31 - 1, -(2 ** 31))) + self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) for frac in values ) @@ -1670,7 +1670,7 @@ def _save(im, fp, filename): strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip ifd[ROWSPERSTRIP] = rows_per_strip - if strip_byte_counts >= 2 ** 16: + if strip_byte_counts >= 2**16: ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( stride * im.size[1] - strip_byte_counts * (strips_per_image - 1), diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index c32cc52f8..2f54cdebb 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -21,7 +21,6 @@ from . import Image, ImageFile from ._binary import i16le as word -from ._binary import i32le as dword from ._binary import si16le as short from ._binary import si32le as _long @@ -112,7 +111,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): if s[22:26] != b"\x01\x00\t\x00": raise SyntaxError("Unsupported WMF file format") - elif dword(s) == 1 and s[40:44] == b" EMF": + elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF": # enhanced metafile # get bounding box diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 644cfb39b..15379ce80 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -25,7 +25,7 @@ from . import Image, ImageFile # XBM header xbm_head = re.compile( - br"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" + rb"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" b"#define[ \t]+.*_height[ \t]+(?P[0-9]+)[\r\n]+" b"(?P" b"#define[ \t]+[^_]*_x_hot[ \t]+(?P[0-9]+)[\r\n]+" @@ -52,18 +52,19 @@ class XbmImageFile(ImageFile.ImageFile): m = xbm_head.match(self.fp.read(512)) - if m: + if not m: + raise SyntaxError("not a XBM file") - xsize = int(m.group("width")) - ysize = int(m.group("height")) + xsize = int(m.group("width")) + ysize = int(m.group("height")) - if m.group("hotspot"): - self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) + if m.group("hotspot"): + self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) - self.mode = "1" - self._size = xsize, ysize + self.mode = "1" + self._size = xsize, ysize - self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] + self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] def _save(im, fp, filename): 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 0f200af6b..ba57deca1 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -37,7 +37,7 @@ #define MAX(a, b) (a) > (b) ? (a) : (b) #define MIN(a, b) (a) < (b) ? (a) : (b) -#define CLIP16(v) ((v) <= -32768 ? -32768 : (v) >= 32767 ? 32767 : (v)) +#define CLIP16(v) ((v) <= 0 ? 0 : (v) >= 65535 ? 65535 : (v)) /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ #define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114) @@ -1634,29 +1634,15 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { return (Imaging)ImagingError_ModeError(); } - if (!((strcmp(imIn->mode, "RGB") == 0 || strcmp(imIn->mode, "1") == 0 || - strcmp(imIn->mode, "I") == 0 || strcmp(imIn->mode, "L") == 0) && - strcmp(mode, "RGBA") == 0)) -#ifdef notdef - { - return (Imaging)ImagingError_ValueError("conversion not supported"); - } -#else - { - static char buf[100]; - snprintf( - buf, - 100, - "conversion from %.10s to %.10s not supported in convert_transparent", - imIn->mode, - mode); - return (Imaging)ImagingError_ValueError(buf); - } -#endif - - if (strcmp(imIn->mode, "RGB") == 0) { + if (strcmp(imIn->mode, "RGB") == 0 && strcmp(mode, "RGBA") == 0) { convert = rgb2rgba; - } else { + } else if ((strcmp(imIn->mode, "1") == 0 || + strcmp(imIn->mode, "I") == 0 || + strcmp(imIn->mode, "L") == 0 + ) && ( + strcmp(mode, "RGBA") == 0 || + strcmp(mode, "LA") == 0 + )) { if (strcmp(imIn->mode, "1") == 0) { convert = bit2rgb; } else if (strcmp(imIn->mode, "I") == 0) { @@ -1665,6 +1651,15 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { convert = l2rgb; } g = b = r; + } else { + static char buf[100]; + snprintf( + buf, + 100, + "conversion from %.10s to %.10s not supported in convert_transparent", + imIn->mode, + mode); + return (Imaging)ImagingError_ValueError(buf); } imOut = ImagingNew2Dirty(mode, imOut, imIn); diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index be26cd260..fafd8141e 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -295,7 +295,7 @@ ImagingPaste( paste_mask_L(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - } else if (strcmp(imMask->mode, "RGBA") == 0) { + } else if (strcmp(imMask->mode, "LA") == 0 || strcmp(imMask->mode, "RGBA") == 0) { ImagingSectionEnter(&cookie); paste_mask_RGBA( imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); 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/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 ade620347..31a3fbab7 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( [ @@ -280,9 +280,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/3.4.0.zip", - "filename": "harfbuzz-3.4.0.zip", - "dir": "harfbuzz-3.4.0", + "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"), @@ -464,7 +464,7 @@ def build_dep_all(): if dep_name in disabled: continue script = build_dep(dep_name) - lines.append(fr'cmd.exe /c "{{build_dir}}\{script}"') + lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') lines.append("if errorlevel 1 echo Build failed! && exit /B 1") lines.append("@echo All Pillow dependencies built successfully!") write_script("build_dep_all.cmd", lines)