diff --git a/.appveyor.yml b/.appveyor.yml index b817cd9d8..d4dd2dc95 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -21,9 +21,11 @@ environment: install: - '%PYTHON%\%EXECUTABLE% --version' - curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip +- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-depends.zip -oc:\ +- 7z x pillow-test-images.zip -oc:\ - mv c:\pillow-depends-main c:\pillow-depends -- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images +- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ - ..\pillow-depends\gs1000w32.exe /S - path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH% diff --git a/.ci/install.sh b/.ci/install.sh index 518b66acc..6aa122cc5 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -37,7 +37,8 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma if [[ $(uname) != CYGWIN* ]]; then - python3 -m pip install numpy + # TODO Remove condition when NumPy supports 3.12 + if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index db0307046..560d6c7df 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -3,10 +3,12 @@ name: CIFuzz on: push: paths: + - ".github/workflows/cifuzz.yml" - "**.c" - "**.h" pull_request: paths: + - ".github/workflows/cifuzz.yml" - "**.c" - "**.h" workflow_dispatch: @@ -14,7 +16,7 @@ on: permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..8a3265476 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,52 @@ +name: Docs + +on: + push: + paths: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + + runs-on: ubuntu-latest + name: Docs + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: ".ci/*.sh" + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Install Linux dependencies + run: | + .ci/install.sh + env: + GHA_PYTHON_VERSION: "3.x" + + - name: Build + run: | + .ci/build.sh + + - name: Docs + run: | + make doccheck diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index dfd7d0553..1fc6262f4 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -13,7 +13,8 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -python3 -m pip install numpy +# TODO Remove condition when NumPy supports 3.12 +if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 1dfb36f44..6c9ed66e3 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -1,6 +1,15 @@ name: Test Cygwin -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -59,7 +68,7 @@ jobs: python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter qt5-devel-tools - subversion + wget xorg-server-extra zlib-devel diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 7331cf8ee..f7153386e 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -1,6 +1,15 @@ name: Test Docker -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -87,6 +96,7 @@ jobs: with: flags: GHA_Docker name: ${{ matrix.docker }} + gcov: true success: permissions: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 24575f6c7..ddfafc9d7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -1,6 +1,15 @@ name: Test MinGW -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -59,8 +68,7 @@ jobs: ${{ matrix.package }}-python3-numpy \ ${{ matrix.package }}-python3-olefile \ ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools \ - subversion + ${{ matrix.package }}-python3-setuptools if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then pacman -S --noconfirm \ diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index f8b050f76..6fab0ecd2 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -5,10 +5,12 @@ name: Test Valgrind on: push: paths: + - ".github/workflows/test-valgrind.yml" - "**.c" - "**.h" pull_request: paths: + - ".github/workflows/test-valgrind.yml" - "**.c" - "**.h" workflow_dispatch: @@ -16,7 +18,7 @@ on: permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 487c3586f..833f096c3 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -1,6 +1,15 @@ name: Test Windows -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -15,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows @@ -38,6 +47,12 @@ jobs: repository: python-pillow/pillow-depends path: winbuild\depends + - name: Checkout extra test images + uses: actions/checkout@v3 + with: + repository: python-pillow/test-images + path: Tests\test-images + # sets env: pythonLocation - name: Set up Python uses: actions/setup-python@v4 @@ -62,7 +77,8 @@ jobs: winbuild\depends\gs1000w32.exe /S echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH - xcopy /S /Y winbuild\depends\test_images\* Tests\images\ + # Install extra test images + xcopy /S /Y Tests\test-images\* Tests\images # make cache key depend on VS version & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11c7b77be..10c3cd929 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,15 @@ name: Test -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -22,6 +31,7 @@ jobs: python-version: [ "pypy3.9", "pypy3.8", + "3.12-dev", "3.11", "3.10", "3.9", @@ -95,11 +105,6 @@ jobs: name: errors path: Tests/errors - - name: Docs - if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.11 - run: | - make doccheck - - name: After success run: | .ci/after_success.sh @@ -107,9 +112,9 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v3 with: - file: ./coverage.xml flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} + gcov: true success: permissions: diff --git a/.gitignore b/.gitignore index 790404535..1dd6c9175 100644 --- a/.gitignore +++ b/.gitignore @@ -79,7 +79,7 @@ docs/_build/ # JetBrains .idea -# Extra test images installed from pillow-depends/test_images +# Extra test images installed from python-pillow/test-images Tests/images/README.md Tests/images/crash_1.tif Tests/images/crash_2.tif diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5214d352d..45c1f3c5f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.1.0 hooks: - id: black args: [--target-version=py37] diff --git a/CHANGES.rst b/CHANGES.rst index e35a55965..cbf91baff 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,39 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Close OleFileIO instance when closing or exiting FPX or MIC #7005 + [radarhere] + +- Added __int__ to IFDRational for Python >= 3.11 #6998 + [radarhere] + +- Added memoryview support to Dib.frombytes() #6988 + [radarhere, nulano] + +- Close file pointer copy in the libtiff encoder if still open #6986 + [fcarron, radarhere] + +- Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978 + [radarhere] + +- Added "corners" argument to ImageDraw rounded_rectangle() #6954 + [radarhere] + +- Added memoryview support to frombytes() #6974 + [radarhere] + +- Allow comments in FITS images #6973 + [radarhere] + +- Support saving PDF with different X and Y resolutions #6961 + [jvanderneutstulen, radarhere, hugovk] + +- Fixed writing int as UNDEFINED tag #6950 + [radarhere] + +- Raise an error if EXIF data is too long when saving JPEG #6939 + [radarhere] + - Handle more than one directory returned by pkg-config #6896 [sebastic, radarhere] diff --git a/LICENSE b/LICENSE index 125bdcc44..cf65e86d7 100644 --- a/LICENSE +++ b/LICENSE @@ -13,8 +13,8 @@ By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: -Permission to use, copy, modify, and distribute this software and its -associated documentation for any purpose and without fee is hereby granted, +Permission to use, copy, modify and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 08a55d349..c600c45ed 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -4,7 +4,6 @@ TEST_FILE = "Tests/images/fli_overflow.fli" def test_fli_overflow(): - # this should not crash with a malloc error or access violation with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index d8d645189..f4a129f50 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -23,7 +23,6 @@ def test_ignore_dos_text(): def test_dos_text(): - try: im = Image.open(TEST_FILE) im.load() diff --git a/Tests/helper.py b/Tests/helper.py index 0d1d03ac8..69246bfcf 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) HAS_UPLOADER = False -if os.environ.get("SHOW_ERRORS", None): +if os.environ.get("SHOW_ERRORS"): # local img.show for errors. HAS_UPLOADER = True @@ -271,7 +271,7 @@ def netpbm_available(): def magick_command(): if sys.platform == "win32": - magickhome = os.environ.get("MAGICK_HOME", "") + magickhome = os.environ.get("MAGICK_HOME") if magickhome: imagemagick = [os.path.join(magickhome, "convert.exe")] graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"] diff --git a/Tests/images/imagedraw_ellipse_various_sizes.png b/Tests/images/imagedraw_ellipse_various_sizes.png index 11a1be6fa..5e3cf22b4 100644 Binary files a/Tests/images/imagedraw_ellipse_various_sizes.png and b/Tests/images/imagedraw_ellipse_various_sizes.png differ diff --git a/Tests/images/imagedraw_ellipse_various_sizes_filled.png b/Tests/images/imagedraw_ellipse_various_sizes_filled.png index d71e175b8..dd2f641f1 100644 Binary files a/Tests/images/imagedraw_ellipse_various_sizes_filled.png and b/Tests/images/imagedraw_ellipse_various_sizes_filled.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png new file mode 100644 index 000000000..3e79e21ae Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnny.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnny.png new file mode 100644 index 000000000..7fa09a3c0 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nnny.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png new file mode 100644 index 000000000..d825ad263 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png new file mode 100644 index 000000000..c19da698e Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png new file mode 100644 index 000000000..f3e95d487 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png new file mode 100644 index 000000000..274d27984 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png new file mode 100644 index 000000000..c5f40bfdb Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png new file mode 100644 index 000000000..01bfd1750 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png new file mode 100644 index 000000000..efd27be4f Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png new file mode 100644 index 000000000..d3acd01ab Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png new file mode 100644 index 000000000..55ddbc033 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png new file mode 100644 index 000000000..c000b26e9 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png new file mode 100644 index 000000000..7056b4fd9 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yyny.png b/Tests/images/imagedraw_rounded_rectangle_corners_yyny.png new file mode 100644 index 000000000..5eca030b9 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_yyny.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png new file mode 100644 index 000000000..7f1f00344 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png new file mode 100644 index 000000000..2e815f4ad Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png differ diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index ed9aff9cc..002a44a4f 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -18,7 +18,6 @@ def test_bad(): """These shouldn't crash/dos, but they shouldn't return anything either""" for f in get_files("b"): - # Assert that there is no unclosed file warning with warnings.catch_warnings(): try: diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 385192a3c..9021a9fb3 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -177,13 +177,14 @@ class TestEnvVars: Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) assert Image.core.get_block_size() == 2 * 1024 * 1024 - def test_warnings(self): - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_ALIGNMENT": "15"} - ) - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_BLOCK_SIZE": "1024"} - ) - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_BLOCKS_MAX": "wat"} - ) + @pytest.mark.parametrize( + "var", + ( + {"PILLOW_ALIGNMENT": "15"}, + {"PILLOW_BLOCK_SIZE": "1024"}, + {"PILLOW_BLOCKS_MAX": "wat"}, + ), + ) + def test_warnings(self, var): + with pytest.warns(UserWarning): + Image._apply_env_variables(var) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 63071b78c..4fd02449c 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -36,12 +36,10 @@ class TestDecompressionBomb: Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 - def open(): + with pytest.warns(Image.DecompressionBombWarning): with Image.open(TEST_FILE): pass - pytest.warns(Image.DecompressionBombWarning, open) - def test_exception(self): # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 @@ -87,7 +85,8 @@ class TestDecompressionCrop: # same decompression bomb warnings on them. with hopper() as src: box = (0, 0, src.width * 2, src.height * 2) - pytest.warns(Image.DecompressionBombWarning, src.crop, box) + with pytest.warns(Image.DecompressionBombWarning): + src.crop(box) def test_crop_decompression_checks(self): im = Image.new("RGB", (100, 100)) @@ -95,7 +94,8 @@ class TestDecompressionCrop: for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): assert im.crop(value).size == (9, 9) - pytest.warns(Image.DecompressionBombWarning, im.crop, (-160, -160, 99, 99)) + with pytest.warns(Image.DecompressionBombWarning): + im.crop((-160, -160, 99, 99)) with pytest.raises(Image.DecompressionBombError): im.crop((-99909, -99990, 99999, 99999)) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 51637c786..b2bec5984 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -263,13 +263,11 @@ def test_apng_chunk_errors(): with Image.open("Tests/images/apng/chunk_no_actl.png") as im: assert not im.is_animated - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: im.load() assert not im.is_animated - pytest.warns(UserWarning, open) - with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: assert not im.is_animated @@ -287,21 +285,17 @@ def test_apng_chunk_errors(): def test_apng_syntax_errors(): - def open_frames_zero(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: assert not im.is_animated with pytest.raises(OSError): im.load() - pytest.warns(UserWarning, open_frames_zero) - - def open_frames_zero_default(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: assert not im.is_animated im.load() - pytest.warns(UserWarning, open_frames_zero_default) - # we can handle this case gracefully exception = None with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: @@ -316,13 +310,11 @@ def test_apng_syntax_errors(): im.seek(im.n_frames - 1) im.load() - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: assert not im.is_animated im.load() - pytest.warns(UserWarning, open) - @pytest.mark.parametrize( "test_file", diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 5f6d52355..9e79937e9 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -141,7 +141,6 @@ def test_rgba_bitfields(): # This test image has been manually hexedited # to change the bitfield compression in the header from XBGR to RGBA with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: - # So before the comparing the image, swap the channels b, g, r = im.split()[1:] im = Image.merge("RGB", (r, g, b)) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index e330404d6..a7714c92c 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "BUFR" @@ -31,7 +30,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() @@ -58,6 +56,7 @@ def test_handler(tmp_path): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) def save(self, im, fp, filename): diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 0f09c4b99..22686af34 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -15,7 +15,6 @@ def test_sanity(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.size == (128, 128) assert isinstance(im, DcxImagePlugin.DcxImageFile) @@ -29,7 +28,8 @@ def test_unclosed_file(): im = Image.open(TEST_FILE) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -54,7 +54,6 @@ def test_invalid_file(): def test_tell(): # Arrange with Image.open(TEST_FILE) as im: - # Act frame = im.tell() diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 015dda992..ac6e84447 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -80,7 +80,6 @@ def test_invalid_file(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_cmyk(): with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: - assert cmyk_image.mode == "CMYK" assert cmyk_image.size == (100, 100) assert cmyk_image.format == "EPS" diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 447888acd..6f988729f 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -12,7 +12,6 @@ TEST_FILE = "Tests/images/hopper.fits" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "FITS" assert im.size == (128, 128) @@ -45,6 +44,12 @@ def test_naxis_zero(): pass +def test_comment(): + image_data = b"SIMPLE = T / comment string" + with pytest.raises(OSError): + FitsImagePlugin.FitsImageFile(BytesIO(image_data)) + + def test_stub_deprecated(): class Handler: opened = False @@ -55,6 +60,7 @@ def test_stub_deprecated(): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) handler = Handler() diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index b8b999d70..f96afdc95 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -36,7 +36,8 @@ def test_unclosed_file(): im = Image.open(static_test_file) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -64,7 +65,6 @@ def test_context_manager(): def test_tell(): # Arrange with Image.open(static_test_file) as im: - # Act frame = im.tell() @@ -110,7 +110,6 @@ def test_eoferror(): def test_seek_tell(): with Image.open(animated_test_file) as im: - layer_number = im.tell() assert layer_number == 0 diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index fa22e90f6..9a1784d31 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -18,6 +18,16 @@ def test_sanity(): assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") +def test_close(): + with Image.open("Tests/images/input_bw_one_band.fpx") as im: + pass + assert im.ole.fp.closed + + im = Image.open("Tests/images/input_bw_one_band.fpx") + im.close() + assert im.ole.fp.closed + + def test_invalid_file(): # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 6fbc0ee30..8522f486a 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -36,7 +36,8 @@ def test_unclosed_file(): im = Image.open(TEST_GIF) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -209,7 +210,7 @@ def test_optimize_if_palette_can_be_reduced_by_half(): im = im.resize((591, 443)) im_rgb = im.convert("RGB") - for (optimize, colors) in ((False, 256), (True, 8)): + for optimize, colors in ((False, 256), (True, 8)): out = BytesIO() im_rgb.save(out, "GIF", optimize=optimize) with Image.open(out) as reloaded: @@ -221,7 +222,6 @@ def test_roundtrip(tmp_path): im = hopper() im.save(out) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), im, 50) @@ -232,7 +232,6 @@ def test_roundtrip2(tmp_path): im2 = im.copy() im2.save(out) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), hopper(), 50) @@ -242,7 +241,6 @@ def test_roundtrip_save_all(tmp_path): im = hopper() im.save(out, save_all=True) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), im, 50) # Multiframe image @@ -284,13 +282,11 @@ def test_headers_saving_for_animated_gifs(tmp_path): important_headers = ["background", "version", "duration", "loop"] # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - info = im.info.copy() out = str(tmp_path / "temp.gif") im.save(out, save_all=True) with Image.open(out) as reread: - for header in important_headers: assert info[header] == reread.info[header] @@ -308,7 +304,6 @@ def test_palette_handling(tmp_path): im2.save(f, optimize=True) with Image.open(f) as reloaded: - assert_image_similar(im, reloaded.convert("RGB"), 10) @@ -324,7 +319,6 @@ def test_palette_434(tmp_path): orig = "Tests/images/test.colors.gif" with Image.open(orig) as im: - with roundtrip(im) as reloaded: assert_image_similar(im, reloaded, 1) with roundtrip(im, optimize=True) as reloaded: @@ -575,7 +569,6 @@ def test_save_dispose(tmp_path): ) with Image.open(out) as img: - for i in range(2): img.seek(img.tell() + 1) assert img.disposal_method == i + 1 @@ -773,7 +766,6 @@ def test_multiple_duration(tmp_path): out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: - for duration in duration_list: assert reread.info["duration"] == duration try: @@ -786,7 +778,6 @@ def test_multiple_duration(tmp_path): out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list) ) with Image.open(out) as reread: - for duration in duration_list: assert reread.info["duration"] == duration try: @@ -844,7 +835,6 @@ def test_identical_frames(tmp_path): out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: - # Assert that the first three frames were combined assert reread.n_frames == 2 @@ -1098,7 +1088,8 @@ def test_rgb_transparency(tmp_path): im = Image.new("RGB", (1, 1)) im.info["transparency"] = b"" ims = [Image.new("RGB", (1, 1))] - pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims) + with pytest.warns(UserWarning): + im.save(out, save_all=True, append_images=ims) with Image.open(out) as reloaded: assert "transparency" not in reloaded.info diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index fd427746e..dd1c5e7d2 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "GRIB" @@ -31,7 +30,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() @@ -58,6 +56,7 @@ def test_handler(tmp_path): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) def save(self, im, fp, filename): diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 20b4b9619..7ca10fac5 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -8,7 +8,6 @@ TEST_FILE = "Tests/images/hdf5.h5" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "HDF5" @@ -29,7 +28,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() @@ -59,6 +57,7 @@ def test_handler(tmp_path): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) def save(self, im, fp, filename): diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 55632909c..42275424d 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -16,7 +16,6 @@ def test_sanity(): # Loading this icon by default should result in the largest size # (512x512@2x) being loaded with Image.open(TEST_FILE) as im: - # Assert that there is no unclosed file warning with warnings.catch_warnings(): im.load() diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index afb17b1af..4e6dbe6ed 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -175,7 +175,6 @@ def test_save_256x256(tmp_path): # Act im.save(outfile) with Image.open(outfile) as im_saved: - # Assert assert im_saved.size == (256, 256) @@ -213,12 +212,10 @@ def test_save_append_images(tmp_path): def test_unexpected_size(): # This image has been manually hexedited to state that it is 16x32 # while the image within is still 16x16 - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/hopper_unexpected.ico") as im: assert im.size == (16, 16) - pytest.warns(UserWarning, open) - def test_draw_reloaded(tmp_path): with Image.open(TEST_ICO_FILE) as im: diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 5cf93713b..fd00f260e 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -32,7 +32,8 @@ def test_unclosed_file(): im = Image.open(TEST_IM) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -51,7 +52,6 @@ def test_context_manager(): def test_tell(): # Arrange with Image.open(TEST_IM) as im: - # Act frame = im.tell() diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 2d0e6977a..2d99528d3 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -11,7 +11,6 @@ TEST_FILE = "Tests/images/iptc.jpg" def test_getiptcinfo_jpg_none(): # Arrange with hopper() as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) @@ -22,7 +21,6 @@ def test_getiptcinfo_jpg_none(): def test_getiptcinfo_jpg_found(): # Arrange with Image.open(TEST_FILE) as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) @@ -35,7 +33,6 @@ def test_getiptcinfo_jpg_found(): def test_getiptcinfo_tiff_none(): # Arrange with Image.open("Tests/images/hopper.tif") as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index eabc6bf75..4981e15af 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -57,7 +57,6 @@ class TestFileJpeg: return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) def test_sanity(self): - # internal version number assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) @@ -271,7 +270,10 @@ class TestFileJpeg: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") im = hopper() - im.save(f, "JPEG", quality=90, exif=b"1" * 65532) + im.save(f, "JPEG", quality=90, exif=b"1" * 65533) + + with pytest.raises(ValueError): + im.save(f, "JPEG", quality=90, exif=b"1" * 65534) def test_exif_typeerror(self): with Image.open("Tests/images/exif_typeerror.jpg") as im: @@ -368,7 +370,6 @@ class TestFileJpeg: def test_exif_gps_typeerror(self): with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: - # Should not raise a TypeError im._getexif() @@ -447,7 +448,7 @@ class TestFileJpeg: ims = im.get_child_images() assert len(ims) == 1 - assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png") + assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1) def test_mp(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: @@ -682,7 +683,6 @@ class TestFileJpeg: # Shouldn't raise error fn = "Tests/images/sugarshack_bad_mpo_header.jpg" with pytest.warns(UserWarning, Image.open, fn) as im: - # Assert assert im.format == "JPEG" @@ -704,7 +704,6 @@ class TestFileJpeg: # Arrange outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: - # Act im.save(outfile, "JPEG", dpi=im.info["dpi"]) @@ -731,7 +730,6 @@ class TestFileJpeg: # This Photoshop CC 2017 image has DPI in EXIF not metadata # EXIF XResolution is (2000000, 10000) with Image.open("Tests/images/photoshop-200dpi.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (200, 200) @@ -740,7 +738,6 @@ class TestFileJpeg: # This image has DPI in EXIF not metadata # EXIF XResolution is 72 with Image.open("Tests/images/exif-72dpi-int.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (72, 72) @@ -749,7 +746,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg with Image.open("Tests/images/exif-200dpcm.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (508, 508) @@ -758,7 +754,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg with Image.open("Tests/images/exif-dpi-zerodivision.jpg") as im: - # Act / Assert # This should return the default, and not raise a ZeroDivisionError assert im.info.get("dpi") == (72, 72) @@ -767,7 +762,6 @@ class TestFileJpeg: # Arrange # 0x011A tag in this exif contains string '300300\x02' with Image.open("Tests/images/broken_exif_dpi.jpg") as im: - # Act / Assert # This should return the default assert im.info.get("dpi") == (72, 72) @@ -777,7 +771,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with resolution removed from EXIF: # exiftool "-*resolution*"= photoshop-200dpi.jpg with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: - # Act / Assert # "When the image resolution is unknown, 72 [dpi] is designated." # https://exiv2.org/tags.html @@ -787,7 +780,6 @@ class TestFileJpeg: # This is no-dpi-in-exif with the tiff header of the exif block # hexedited from MM * to FF FF FF FF with Image.open("Tests/images/invalid-exif.jpg") as im: - # This should return the default, and not a SyntaxError or # OSError for unidentified image. assert im.info.get("dpi") == (72, 72) @@ -810,7 +802,6 @@ class TestFileJpeg: def test_invalid_exif_x_resolution(self): # When no x or y resolution is defined in EXIF with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: - # This should return the default, and not a ValueError or # OSError for an unidentified image. assert im.info.get("dpi") == (72, 72) @@ -820,7 +811,6 @@ class TestFileJpeg: # This image has been manually hexedited to have an IFD offset of 10, # in contrast to normal 8 with Image.open("Tests/images/exif-ifd-offset.jpg") as im: - # Act / Assert assert im._getexif()[306] == "2017:03:13 23:03:09" diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 0229b2243..de622c478 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -270,7 +270,6 @@ def test_rgba(): # Arrange with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: - # Act j2k.load() jp2.load() diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 1109cd15e..871ad28b5 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -645,7 +645,6 @@ class TestFileLibTiff(LibTiffTestCase): pilim = hopper() def save_bytesio(compression=None): - buffer_io = io.BytesIO() pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) @@ -740,7 +739,6 @@ class TestFileLibTiff(LibTiffTestCase): def test_multipage_compression(self): with Image.open("Tests/images/compression.tif") as im: - im.seek(0) assert im._compression == "tiff_ccitt" assert im.size == (10, 10) @@ -1067,3 +1065,9 @@ class TestFileLibTiff(LibTiffTestCase): out = str(tmp_path / "temp.tif") with pytest.raises(SystemError): im.save(out, compression=compression) + + def test_save_many_compressed(self, tmp_path): + im = hopper() + out = str(tmp_path / "temp.tif") + for _ in range(10000): + im.save(out, compression="jpeg") diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 464d138e2..2588d3a05 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -51,6 +51,16 @@ def test_seek(): assert im.tell() == 0 +def test_close(): + with Image.open(TEST_FILE) as im: + pass + assert im.ole.fp.closed + + im = Image.open(TEST_FILE) + im.close() + assert im.ole.fp.closed + + def test_invalid_file(): # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index f0dedc2de..2e921e467 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -42,7 +42,8 @@ def test_unclosed_file(): im = Image.open(test_files[0]) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 50d7c590b..497052b05 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -44,7 +44,6 @@ def test_open_windows_v1(): # Arrange # Act with Image.open(TEST_FILE) as im: - # Assert assert_image_equal(im, hopper("1")) assert isinstance(im, MspImagePlugin.MspImageFile) @@ -59,7 +58,6 @@ def _assert_file_image_equal(source_path, target_path): not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) def test_open_windows_v2(): - files = ( os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 705505e83..967f5c35e 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -85,6 +85,34 @@ def test_resolution(tmp_path): assert size == (61.44, 61.44) +@pytest.mark.parametrize( + "params", + ( + {"dpi": (75, 150)}, + {"dpi": (75, 150), "resolution": 200}, + ), +) +def test_dpi(params, tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, **params) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (122.88, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (122.88, 61.44) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @@ -94,7 +122,6 @@ def test_save_all(tmp_path): # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") im.save(outfile, save_all=True) @@ -128,7 +155,6 @@ def test_save_all(tmp_path): def test_multiframe_normal_save(tmp_path): # Test saving a multiframe image without save_all with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") im.save(outfile) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 133f3e47e..c4db97905 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -78,7 +78,6 @@ class TestFilePng: return chunks def test_sanity(self, tmp_path): - # internal version number assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib")) @@ -156,7 +155,6 @@ class TestFilePng: assert im.info == {"spam": "egg"} def test_bad_itxt(self): - im = load(HEAD + chunk(b"iTXt") + TAIL) assert im.info == {} @@ -201,7 +199,6 @@ class TestFilePng: assert im.info["spam"].tkey == "Spam" def test_interlace(self): - test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) @@ -495,7 +492,6 @@ class TestFilePng: # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" with Image.open(test_file) as im: - assert im.info["transparency"] == 0 def test_save_icc_profile(self): diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 4f934375c..e405834b5 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -27,7 +27,8 @@ def test_unclosed_file(): im = Image.open(test_file) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -77,7 +78,6 @@ def test_eoferror(): def test_seek_tell(): with Image.open(test_file) as im: - layer_number = im.tell() assert layer_number == 1 @@ -95,7 +95,6 @@ def test_seek_tell(): def test_seek_eoferror(): with Image.open(test_file) as im: - with pytest.raises(EOFError): im.seek(-1) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 0e3b705a2..09f1ef8e4 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -25,7 +25,8 @@ def test_unclosed_file(): im = Image.open(TEST_FILE) im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(): @@ -79,7 +80,6 @@ def test_is_spider_image(): def test_tell(): # Arrange with Image.open(TEST_FILE) as im: - # Act index = im.tell() diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 05c78c316..edb320603 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -16,7 +16,6 @@ def test_sanity(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (128, 128) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 799c243d6..b27fa25f3 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -29,11 +29,9 @@ def test_sanity(codec, test_path, format): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") - pytest.warns(ResourceWarning, open) - def test_close(): with warnings.catch_warnings(): diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 7d8b5139a..1a5730f49 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -78,7 +78,6 @@ def test_id_field(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (100, 100) @@ -89,7 +88,6 @@ def test_id_field_rle(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (199, 199) @@ -165,13 +163,14 @@ def test_save_id_section(tmp_path): # Save with custom id section greater than 255 characters id_section = b"Test content" * 25 - pytest.warns(UserWarning, lambda: im.save(out, id_section=id_section)) + with pytest.warns(UserWarning): + im.save(out, id_section=id_section) + with Image.open(out) as test_im: assert test_im.info["id_section"] == id_section[:255] test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: - # Save with no id section im.save(out, id_section="") with Image.open(out) as test_im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 4f3c8e390..b40f690f5 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -25,7 +25,6 @@ except ImportError: class TestFileTiff: def test_sanity(self, tmp_path): - filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename) @@ -62,7 +61,8 @@ class TestFileTiff: im = Image.open("Tests/images/multipage.tiff") im.load() - pytest.warns(ResourceWarning, open) + with pytest.warns(ResourceWarning): + open() def test_closed_file(self): with warnings.catch_warnings(): @@ -157,7 +157,6 @@ class TestFileTiff: def test_xyres_tiff(self): filename = "Tests/images/pil168.tif" with Image.open(filename) as im: - # legacy api assert isinstance(im.tag[X_RESOLUTION][0], tuple) assert isinstance(im.tag[Y_RESOLUTION][0], tuple) @@ -171,7 +170,6 @@ class TestFileTiff: def test_xyres_fallback_tiff(self): filename = "Tests/images/compression.tif" with Image.open(filename) as im: - # v2 api assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) @@ -186,7 +184,6 @@ class TestFileTiff: def test_int_resolution(self): filename = "Tests/images/pil168.tif" with Image.open(filename) as im: - # Try to read a file where X,Y_RESOLUTION are ints im.tag_v2[X_RESOLUTION] = 71 im.tag_v2[Y_RESOLUTION] = 71 @@ -235,7 +232,8 @@ class TestFileTiff: def test_bad_exif(self): with Image.open("Tests/images/hopper_bad_exif.jpg") as i: # Should not raise struct.error. - pytest.warns(UserWarning, i._getexif) + with pytest.warns(UserWarning): + i._getexif() def test_save_rgba(self, tmp_path): im = hopper("RGBA") @@ -381,7 +379,6 @@ class TestFileTiff: def test___str__(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: - # Act ret = str(im.ifd) @@ -392,7 +389,6 @@ class TestFileTiff: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: - # v2 interface v2_tags = { 256: 55, @@ -630,7 +626,6 @@ class TestFileTiff: filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename, **kwargs) with Image.open(filename) as im: - # legacy interface assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[Y_RESOLUTION][0][0] == 36 diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 1061f7d05..b7d100e7a 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -54,7 +54,6 @@ def test_rt_metadata(tmp_path): img.save(f, tiffinfo=info) with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) @@ -74,14 +73,12 @@ def test_rt_metadata(tmp_path): info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) img.save(f, tiffinfo=info) with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) def test_read_metadata(): with Image.open("Tests/images/hopper_g4.tif") as img: - assert { "YResolution": IFDRational(4294967295, 113653537), "PlanarConfiguration": 1, @@ -219,6 +216,22 @@ def test_writing_other_types_to_bytes(value, tmp_path): assert reloaded.tag_v2[700] == b"\x01" +def test_writing_other_types_to_undefined(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[33723] + assert tag.type == TiffTags.UNDEFINED + + info[33723] = 1 + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[33723] == b"1" + + def test_undefined_zero(tmp_path): # Check that the tag has not been changed since this test was created tag = TiffTags.TAGS_V2[45059] @@ -239,7 +252,8 @@ def test_empty_metadata(): head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) # Should not raise struct.error. - pytest.warns(UserWarning, info.load, f) + with pytest.warns(UserWarning): + info.load(f) def test_iccprofile(tmp_path): @@ -405,11 +419,12 @@ def test_too_many_entries(): ifd = TiffImagePlugin.ImageFileDirectory_v2() # 277: ("SamplesPerPixel", SHORT, 1), - ifd._tagdata[277] = struct.pack("hh", 4, 4) + ifd._tagdata[277] = struct.pack(">> im.save('Tests/images/hopper_45.png') with Image.open("Tests/images/hopper_45.png") as target: - for (resample, epsilon) in ( + for resample, epsilon in ( (Image.Resampling.NEAREST, 10), (Image.Resampling.BILINEAR, 5), (Image.Resampling.BICUBIC, 0), diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 178cfcef3..a12ce329f 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -4,7 +4,6 @@ from .helper import assert_image_equal, fromstring, hopper def test_sanity(): - with pytest.raises(ValueError): hopper().tobitmap() diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index b839a7b14..d0fea3854 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -50,7 +50,6 @@ def test_add(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add(im1, im2) @@ -63,7 +62,6 @@ def test_add_scale_offset(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add(im1, im2, scale=2.5, offset=100) @@ -87,7 +85,6 @@ def test_add_modulo(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add_modulo(im1, im2) @@ -111,7 +108,6 @@ def test_blend(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.blend(im1, im2, 0.5) @@ -137,7 +133,6 @@ def test_darker_image(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.darker(im1, im2) @@ -149,7 +144,6 @@ def test_darker_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.darker(im1, im2) @@ -161,7 +155,6 @@ def test_difference(): # Arrange with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1: with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2: - # Act new = ImageChops.difference(im1, im2) @@ -173,7 +166,6 @@ def test_difference_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: - # Act new = ImageChops.difference(im1, im2) @@ -195,7 +187,6 @@ def test_duplicate(): def test_invert(): # Arrange with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: - # Act new = ImageChops.invert(im) @@ -209,7 +200,6 @@ def test_lighter_image(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.lighter(im1, im2) @@ -221,7 +211,6 @@ def test_lighter_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.lighter(im1, im2) @@ -275,7 +264,6 @@ def test_offset(): xoffset = 45 yoffset = 20 with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im: - # Act new = ImageChops.offset(im, xoffset, yoffset) @@ -292,7 +280,6 @@ def test_screen(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.screen(im1, im2) @@ -305,7 +292,6 @@ def test_subtract(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2) @@ -319,7 +305,6 @@ def test_subtract_scale_offset(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) @@ -332,7 +317,6 @@ def test_subtract_clip(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2) @@ -344,7 +328,6 @@ def test_subtract_modulo(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract_modulo(im1, im2) @@ -358,7 +341,6 @@ def test_subtract_modulo_no_clip(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.subtract_modulo(im1, im2) @@ -370,7 +352,6 @@ def test_soft_light(): # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act new = ImageChops.soft_light(im1, im2) @@ -383,7 +364,6 @@ def test_hard_light(): # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act new = ImageChops.hard_light(im1, im2) @@ -396,7 +376,6 @@ def test_overlay(): # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act new = ImageChops.overlay(im1, im2) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 3d8dbe6bb..66be02078 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -625,3 +625,14 @@ def test_constants_deprecation(): for name in enum.__members__: with pytest.warns(DeprecationWarning): assert getattr(ImageCms, prefix + name) == enum[name] + + +@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) +def test_rgb_lab(mode): + im = Image.new(mode, (1, 1)) + converted_im = im.convert("LAB") + assert converted_im.getpixel((0, 0)) == (0, 128, 128) + + im = Image.new("LAB", (1, 1), (255, 0, 0)) + converted_im = im.convert(mode) + assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 4c4c41b7b..5295021a3 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -52,7 +52,6 @@ def test_sanity(): def test_valueerror(): with Image.open("Tests/images/chi.gif") as im: - draw = ImageDraw.Draw(im) draw.line((0, 0), fill=(0, 0, 0)) @@ -356,7 +355,13 @@ def ellipse_various_sizes_helper(filled): for w in ellipse_sizes: y = 1 for h in ellipse_sizes: - border = [x, y, x + w - 1, y + h - 1] + x1 = x + w + if w: + x1 -= 1 + y1 = y + h + if h: + y1 -= 1 + border = [x, y, x1, y1] if filled: draw.ellipse(border, fill="white") else: @@ -736,6 +741,36 @@ def test_rounded_rectangle(xy): assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle.png") +@pytest.mark.parametrize("top_left", (True, False)) +@pytest.mark.parametrize("top_right", (True, False)) +@pytest.mark.parametrize("bottom_right", (True, False)) +@pytest.mark.parametrize("bottom_left", (True, False)) +def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_left): + corners = (top_left, top_right, bottom_right, bottom_left) + + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle( + (10, 20, 190, 180), 30, fill="red", outline="green", width=5, corners=corners + ) + + # Assert + suffix = "".join( + ( + ("y" if top_left else "n"), + ("y" if top_right else "n"), + ("y" if bottom_right else "n"), + ("y" if bottom_left else "n"), + ) + ) + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rounded_rectangle_corners_" + suffix + ".png" + ) + + @pytest.mark.parametrize( "xy, radius, type", [ @@ -903,9 +938,6 @@ def test_square(): img, draw = create_base_image_draw((10, 10)) draw.rectangle((2, 2, 7, 7), BLACK) assert_image_equal_tofile(img, expected, "square as normal rectangle failed") - img, draw = create_base_image_draw((10, 10)) - draw.rectangle((7, 7, 2, 2), BLACK) - assert_image_equal_tofile(img, expected, "square as inverted rectangle failed") def test_triangle_right(): @@ -1470,3 +1502,21 @@ def test_polygon2(): draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") expected = "Tests/images/imagedraw_outline_polygon_RGB.png" assert_image_similar_tofile(im, expected, 1) + + +@pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) +def test_incorrectly_ordered_coordinates(xy): + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + with pytest.raises(ValueError): + draw.arc(xy, 10, 260) + with pytest.raises(ValueError): + draw.chord(xy, 10, 260) + with pytest.raises(ValueError): + draw.ellipse(xy) + with pytest.raises(ValueError): + draw.pieslice(xy, 10, 260) + with pytest.raises(ValueError): + draw.rectangle(xy) + with pytest.raises(ValueError): + draw.rounded_rectangle(xy) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index fc0fbfb9b..412bc10d9 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -30,7 +30,6 @@ SAFEBLOCK = ImageFile.SAFEBLOCK class TestImageFile: def test_parser(self): def roundtrip(format): - im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 306a2f1bf..b115517ac 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -351,7 +351,8 @@ def test_rotated_transposed_font(font, orientation): assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] # text length is undefined for vertical text - pytest.raises(ValueError, draw.textlength, word) + with pytest.raises(ValueError): + draw.textlength(word) @pytest.mark.parametrize( @@ -872,25 +873,23 @@ def test_anchor_invalid(font): d.font = font for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: - pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor)) - pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor)) - pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor)) - pytest.raises(ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor)) - pytest.raises( - ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), - ) + with pytest.raises(ValueError): + font.getmask2("hello", anchor=anchor) + with pytest.raises(ValueError): + font.getbbox("hello", anchor=anchor) + with pytest.raises(ValueError): + d.text((0, 0), "hello", anchor=anchor) + with pytest.raises(ValueError): + d.textbbox((0, 0), "hello", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) for anchor in ["lt", "lb"]: - pytest.raises( - ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), - ) + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index cf039e86e..6099b04e4 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -360,37 +360,20 @@ def test_anchor_invalid_ttb(): d.font = font for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]: - pytest.raises( - ValueError, lambda: font.getmask2("hello", anchor=anchor, direction="ttb") - ) - pytest.raises( - ValueError, lambda: font.getbbox("hello", anchor=anchor, direction="ttb") - ) - pytest.raises( - ValueError, lambda: d.text((0, 0), "hello", anchor=anchor, direction="ttb") - ) - pytest.raises( - ValueError, - lambda: d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb"), - ) - pytest.raises( - ValueError, - lambda: d.multiline_text( - (0, 0), "foo\nbar", anchor=anchor, direction="ttb" - ), - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox( - (0, 0), "foo\nbar", anchor=anchor, direction="ttb" - ), - ) + with pytest.raises(ValueError): + font.getmask2("hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + font.getbbox("hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.text((0, 0), "hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb") # ttb multiline text does not support anchors at all - pytest.raises( - ValueError, - lambda: d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb"), - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb"), - ) + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb") + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb") diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index c9b2fd865..d390f3c1e 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -21,7 +21,6 @@ deformer = Deformer() def test_sanity(): - ImageOps.autocontrast(hopper("L")) ImageOps.autocontrast(hopper("RGB")) @@ -419,7 +418,6 @@ def test_autocontrast_cutoff(): def test_autocontrast_mask_toy_input(): # Test the mask argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: - rect_mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(rect_mask) x0 = img.size[0] // 4 @@ -439,7 +437,6 @@ def test_autocontrast_mask_toy_input(): def test_autocontrast_mask_real_input(): # Test the autocontrast with a rectangular mask with Image.open("Tests/images/iptc.jpg") as img: - rect_mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(rect_mask) x0, y0 = img.size[0] // 2, img.size[1] // 2 diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 5bda28117..ac99ef381 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -6,7 +6,6 @@ from .helper import assert_image_equal, assert_image_equal_tofile def test_sanity(): - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) assert len(palette.colors) == 256 @@ -23,7 +22,6 @@ def test_reload(): def test_getcolor(): - palette = ImagePalette.ImagePalette() assert len(palette.palette) == 0 assert len(palette.colors) == 0 @@ -84,7 +82,6 @@ def test_getcolor_not_special(index, palette): def test_file(tmp_path): - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) f = str(tmp_path / "temp.lut") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 861fb64f0..8f8a9f449 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -8,7 +8,6 @@ from PIL import Image, ImagePath def test_path(): - p = ImagePath.Path(list(range(10))) # sequence interface diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 6af7e7602..62f528332 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -6,7 +6,6 @@ from .helper import assert_image_equal, hopper, skip_unless_feature def test_sanity(tmp_path): - test_file = str(tmp_path / "temp.im") im = hopper("RGB") diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 3e147a9ef..eda485cf6 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -55,8 +55,8 @@ def test_show_without_viewers(): viewers = ImageShow._viewers ImageShow._viewers = [] - im = hopper() - assert not ImageShow.show(im) + with hopper() as im: + assert not ImageShow.show(im) ImageShow._viewers = viewers diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 5717fe150..b3b5db13f 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -6,7 +6,6 @@ from .helper import hopper def test_sanity(): - im = hopper() st = ImageStat.Stat(im) @@ -31,7 +30,6 @@ def test_sanity(): def test_hopper(): - im = hopper() st = ImageStat.Stat(im) @@ -45,7 +43,6 @@ def test_hopper(): def test_constant(): - im = Image.new("L", (128, 128), 128) st = ImageStat.Stat(im) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 9d64d17a3..5e489284f 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -100,8 +100,11 @@ class TestImageWinDib: # Act # Make one the same as the using tobytes()/frombytes() test_buffer = dib1.tobytes() - dib2.frombytes(test_buffer) + for datatype in ("bytes", "memoryview"): + if datatype == "memoryview": + test_buffer = memoryview(test_buffer) + dib2.frombytes(test_buffer) - # Assert - # Confirm they're the same - assert dib1.tobytes() == dib2.tobytes() + # Assert + # Confirm they're the same + assert dib1.tobytes() == dib2.tobytes() diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 37ed3659d..f6818be46 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -4,7 +4,6 @@ from PIL import Image def test_setmode(): - im = Image.new("L", (1, 1), 255) im.im.setmode("1") assert im.im.getpixel((0, 0)) == 255 diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index efcdab9ec..dcdee3d41 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -42,7 +42,6 @@ def test_basic(tmp_path, mode): im_in.save(filename) with Image.open(filename) as im_out: - verify(im_in) verify(im_out) @@ -87,7 +86,6 @@ def test_tobytes(): def test_convert(): - im = original.copy() verify(im.convert("I;16")) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 3de7ec30f..a8bbcbdb8 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -235,7 +235,6 @@ def test_no_resource_warning_for_numpy_array(): test_file = "Tests/images/hopper.png" with Image.open(test_file) as im: - # Act/Assert with warnings.catch_warnings(): array(im) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 23eb9e39f..2f6d05888 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -89,7 +89,6 @@ def test_pickle_la_mode_with_palette(tmp_path): def test_pickle_tell(): # Arrange with Image.open("Tests/images/hopper.webp") as image: - # Act: roundtrip unpickled_image = pickle.loads(pickle.dumps(image)) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 34609314c..4929fa933 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_similar(result, expected.convert("RGB"), 0.3) + assert_image_similar(result, expected.convert("RGB"), 1) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 12f475df0..6e3fcec90 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -7,7 +7,6 @@ from .helper import hopper def _test_equal(num, denom, target): - t = IFDRational(num, denom) assert target == t @@ -15,7 +14,6 @@ def _test_equal(num, denom, target): def test_sanity(): - _test_equal(1, 1, 1) _test_equal(1, 1, Fraction(1, 1)) diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 34197c14f..5bd9bacdb 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -9,7 +9,6 @@ test_file = "Tests/images/hopper.webp" @skip_unless_feature("webp") class TestWebPLeaks(PillowLeakTestCase): - mem_limit = 3 * 1024 # kb iterations = 100 diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index d9608e782..a318bfafd 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -8,5 +8,5 @@ if [ ! -f $archive.tar.gz ]; then wget -O $archive.tar.gz $url fi -rm -r $archive +rmdir $archive tar -xvzf $archive.tar.gz diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 02da12d61..1ef6f4e97 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -1,15 +1,12 @@ -#!/bin/bash +#!/usr/bin/env bash # install extra test images -# Use SVN to just fetch a single Git subdirectory -svn_export() -{ - if [ ! -z $1 ]; then - echo "" - echo "Retrying svn export..." - echo "" - fi +archive=test-images-main - svn export --force https://github.com/python-pillow/pillow-depends/trunk/test_images ../Tests/images -} -svn_export || svn_export retry || svn_export retry || svn_export retry +./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/main.tar.gz + +mv $archive/* ../Tests/images/ + +# Cleanup old tarball and empty directory +rm $archive.tar.gz +rmdir $archive diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 8b847b894..362ad95a2 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.1.0 +archive=libimagequant-4.1.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4d48b822a..0db19a64e 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -177,9 +177,7 @@ Deprecated Use :py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -194,9 +192,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont @@ -336,16 +332,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a41ef7cf8..02224af34 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1104,7 +1104,7 @@ using the general tags available through tiffinfo. Either an integer or a float. **dpi** - A tuple of (x_resolution, y_resolution), with inches as the resolution + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution unit. For consistency with other image formats, the x and y resolutions of the dpi will be rounded to the nearest integer. @@ -1126,7 +1126,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: If present and true, instructs the WebP writer to use lossless compression. **quality** - Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest + Integer, 0-100, Defaults to 80. For lossy, 0 gives the smallest size and 100 the largest. For lossless, this parameter is the amount of effort put into the compression: 0 is the fastest, but gives larger files compared to the slowest, but best, 100. @@ -1147,6 +1147,10 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: The exif data to include in the saved file. Only supported if the system WebP library was built with webpmux support. +**xmp** + The XMP data to include in the saved file. Only supported if + the system WebP library was built with webpmux support. + Saving sequences ~~~~~~~~~~~~~~~~ @@ -1389,9 +1393,7 @@ WMF, EMF Pillow can identify WMF and EMF files. On Windows, it can read WMF and EMF files. By default, it will load the image -at 72 dpi. To load it at another resolution: - -.. code-block:: python +at 72 dpi. To load it at another resolution:: from PIL import Image @@ -1400,9 +1402,7 @@ at 72 dpi. To load it at another resolution: To add other read or write support, use :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF -handler. - -.. code-block:: python +handler. :: from PIL import Image from PIL import WmfImagePlugin @@ -1493,6 +1493,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum image, will determine the physical dimensions of the page that will be saved in the PDF. +**dpi** + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution + unit. If both the ``resolution`` parameter and the ``dpi`` parameter are + present, ``resolution`` will be ignored. + **title** The document’s title. If not appending to an existing PDF file, this will default to the filename. diff --git a/docs/handbook/text-anchors.rst b/docs/handbook/text-anchors.rst index 0aecd3483..3a9572ab2 100644 --- a/docs/handbook/text-anchors.rst +++ b/docs/handbook/text-anchors.rst @@ -29,7 +29,7 @@ For example, in the following image, the text is ``ms`` (middle-baseline) aligne :alt: ms (middle-baseline) aligned text. :align: left -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 59dfac588..75604e17a 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -108,9 +108,7 @@ Note that the image plugin must be explicitly registered using :py:func:`PIL.Image.register_open`. Although not required, it is also a good idea to register any extensions used by this format. -Once the plugin has been imported, it can be used: - -.. code-block:: python +Once the plugin has been imported, it can be used:: from PIL import Image import SpamImagePlugin @@ -169,9 +167,7 @@ The raw decoder The ``raw`` decoder is used to read uncompressed data from an image file. It can be used with most uncompressed file formats, such as PPM, BMP, uncompressed TIFF, and many others. To use the raw decoder with the -:py:func:`PIL.Image.frombytes` function, use the following syntax: - -.. code-block:: python +:py:func:`PIL.Image.frombytes` function, use the following syntax:: image = Image.frombytes( mode, size, data, "raw", @@ -281,9 +277,7 @@ decoder that can be used to read various packed formats into a floating point image memory. To use the bit decoder with the :py:func:`PIL.Image.frombytes` function, use -the following syntax: - -.. code-block:: python +the following syntax:: image = Image.frombytes( mode, size, data, "bit", diff --git a/docs/installation.rst b/docs/installation.rst index ea8722c56..1d38919b1 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -150,7 +150,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.14**. + above uses liblcms2. Tested with **1.19** and **2.7-2.15**. * **libwebp** provides the WebP format. @@ -169,7 +169,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.1** + * Pillow has been tested with libimagequant **2.6-4.1.1** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. @@ -186,8 +186,8 @@ Many of Pillow's features require external libraries: * Pillow wheels since version 8.2.0 include a modified version of libraqm that loads libfribidi at runtime if it is installed. On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` - into a directory listed in the `Dynamic-Link Library Search Order (Microsoft Docs) - `_ + into a directory listed in the `Dynamic-link library search order (Microsoft Learn) + `_ (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). See `Build Options`_ to see how to build this version. * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. @@ -442,21 +442,23 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | PyPy3 | | +| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | PyPy3 | | +| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, | -| | | s390x, x86-64 | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.10 | arm64v8, ppc64le, | +| | | s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.7 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | -| | PyPy3 | | +| | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.9 (MinGW) | x86, x86-64 | | +----------------------------+---------------------+ diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index ad0abbbd9..0eba1141a 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -17,9 +17,7 @@ Open, rotate, and display an image (using the default viewer) The following script loads an image, rotates it 45 degrees, and displays it using an external viewer (usually xv on Unix, and the Paint program on -Windows). - -.. code-block:: python +Windows). :: from PIL import Image with Image.open("hopper.jpg") as im: @@ -29,9 +27,7 @@ Create thumbnails ^^^^^^^^^^^^^^^^^ The following script creates nice thumbnails of all JPEG images in the -current directory preserving aspect ratios with 128x128 max resolution. - -.. code-block:: python +current directory preserving aspect ratios with 128x128 max resolution. :: from PIL import Image import glob, os @@ -127,9 +123,7 @@ methods. Unless otherwise stated, all methods return a new instance of the .. automethod:: PIL.Image.Image.convert The following example converts an RGB image (linearly calibrated according to -ITU-R 709, using the D65 luminant) to the CIE XYZ color space: - -.. code-block:: python +ITU-R 709, using the D65 luminant) to the CIE XYZ color space:: rgb2xyz = ( 0.412453, 0.357580, 0.180423, 0, @@ -140,9 +134,7 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.copy .. automethod:: PIL.Image.Image.crop -This crops the input image with the provided coordinates: - -.. code-block:: python +This crops the input image with the provided coordinates:: from PIL import Image @@ -162,9 +154,7 @@ This crops the input image with the provided coordinates: .. automethod:: PIL.Image.Image.entropy .. automethod:: PIL.Image.Image.filter -This blurs the input image using a filter from the ``ImageFilter`` module: - -.. code-block:: python +This blurs the input image using a filter from the ``ImageFilter`` module:: from PIL import Image, ImageFilter @@ -176,9 +166,7 @@ This blurs the input image using a filter from the ``ImageFilter`` module: .. automethod:: PIL.Image.Image.frombytes .. automethod:: PIL.Image.Image.getbands -This helps to get the bands of the input image: - -.. code-block:: python +This helps to get the bands of the input image:: from PIL import Image @@ -187,9 +175,7 @@ This helps to get the bands of the input image: .. automethod:: PIL.Image.Image.getbbox -This helps to get the bounding box coordinates of the input image: - -.. code-block:: python +This helps to get the bounding box coordinates of the input image:: from PIL import Image @@ -217,9 +203,7 @@ This helps to get the bounding box coordinates of the input image: .. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.resize -This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``: - -.. code-block:: python +This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``:: from PIL import Image @@ -231,9 +215,7 @@ This resizes the given image from ``(width, height)`` to ``(width/2, height/2)`` .. automethod:: PIL.Image.Image.rotate -This rotates the input image by ``theta`` degrees counter clockwise: - -.. code-block:: python +This rotates the input image by ``theta`` degrees counter clockwise:: from PIL import Image @@ -256,9 +238,7 @@ This rotates the input image by ``theta`` degrees counter clockwise: .. automethod:: PIL.Image.Image.transpose This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT` -method. - -.. code-block:: python +method. :: from PIL import Image diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 9aa26916a..9565ab149 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -16,7 +16,7 @@ For a more advanced drawing library for PIL, see the `aggdraw module`_. Example: Draw a gray cross over an image ---------------------------------------- -.. code-block:: python +:: import sys from PIL import Image, ImageDraw @@ -78,7 +78,7 @@ libraries, and may not available in all PIL builds. Example: Draw Partial Opacity Text ---------------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -105,7 +105,7 @@ Example: Draw Partial Opacity Text Example: Draw Multiline Text ---------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -318,8 +318,8 @@ Methods Draws a rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box - is inclusive of both endpoints. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param outline: Color to use for the outline. :param fill: Color to use for the fill. :param width: The line width, in pixels. @@ -331,12 +331,14 @@ Methods Draws a rounded rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box - is inclusive of both endpoints. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param radius: Radius of the corners. :param outline: Color to use for the outline. :param fill: Color to use for the fill. :param width: The line width, in pixels. + :param corners: A tuple of whether to round each corner, + ``(top_left, top_right, bottom_right, bottom_left)``. .. versionadded:: 8.2.0 @@ -597,18 +599,14 @@ Methods string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = draw.textlength("Hello", font) world = draw.textlength("World", font) hello_world = hello + world # not adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # may fail - use - - .. code-block:: python + use :: hello = draw.textlength("HelloW", font) - draw.textlength( "W", font @@ -617,9 +615,7 @@ Methods hello_world = hello + world # adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index 29ceee314..b27228ec9 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -10,7 +10,7 @@ for image enhancement. Example: Vary the sharpness of an image --------------------------------------- -.. code-block:: python +:: from PIL import ImageEnhance diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 3cf59c610..047990f1c 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -15,7 +15,7 @@ and **xmllib** modules. Example: Parse an image ----------------------- -.. code-block:: python +:: from PIL import ImageFile diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index c85da4fb5..044aede62 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -11,7 +11,7 @@ filters, which can be be used with the :py:meth:`Image.filter() Example: Filter an image ------------------------ -.. code-block:: python +:: from PIL import ImageFilter diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 516fa63a7..946bd3c4b 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -21,7 +21,7 @@ the imToolkit package. Example ------- -.. code-block:: python +:: from PIL import ImageFont, ImageDraw diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 63f88fddd..118d988d6 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -11,7 +11,7 @@ an expression string and one or more images. Example: Using the :py:mod:`~PIL.ImageMath` module -------------------------------------------------- -.. code-block:: python +:: from PIL import Image, ImageMath diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index b9bdfc507..7c1a3ad70 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -60,9 +60,7 @@ vector data. Path objects can be passed to the methods on the .. py:method:: PIL.ImagePath.Path.transform(matrix) Transforms the path in place, using an affine transform. The matrix is a - 6-tuple (a, b, c, d, e, f), and each point is mapped as follows: - - .. code-block:: python + 6-tuple (a, b, c, d, e, f), and each point is mapped as follows:: xOut = xIn * a + yIn * b + c yOut = xIn * d + yIn * e + f diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index f2e7d9edd..a27b2fb4e 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -10,7 +10,7 @@ iterate over the frames of an image sequence. Extracting frames from an animation ----------------------------------- -.. code-block:: python +:: from PIL import Image, ImageSequence diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index 2ee3cadb7..4151be4a7 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -9,9 +9,7 @@ Windows. ImageWin can be used with PythonWin and other user interface toolkits that provide access to Windows device contexts or window handles. For example, -Tkinter makes the window handle available via the winfo_id method: - -.. code-block:: python +Tkinter makes the window handle available via the winfo_id method:: from PIL import ImageWin diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index b234b7b4e..04d6f5dcd 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -18,9 +18,7 @@ Example ------- The following script loads an image, accesses one pixel from it, then -changes it. - -.. code-block:: python +changes it. :: from PIL import Image @@ -35,9 +33,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index f9eb9b524..ed58ca3a5 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -17,9 +17,7 @@ The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the Example ------- -The following script loads an image, accesses one pixel from it, then changes it. - -.. code-block:: python +The following script loads an image, accesses one pixel from it, then changes it. :: from PIL import Image @@ -34,9 +32,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 6bfd50588..f31941c9a 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -61,9 +61,7 @@ Image Lifecycle * ``Image.Image.close()`` Closes the file and destroys the core image object. The Pillow context manager will also close the file, but will not destroy - the core image object. e.g.: - -.. code-block:: python + the core image object. e.g.:: with Image.open("test.jpg") as img: img.load() diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst index eb4304843..76e13b061 100644 --- a/docs/releasenotes/6.1.0.rst +++ b/docs/releasenotes/6.1.0.rst @@ -13,16 +13,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been dep Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Deprecated: - -.. code-block:: python +Deprecated:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") @@ -79,9 +75,7 @@ Image quality for JPEG compressed TIFF The TIFF encoder accepts a ``quality`` parameter for ``jpeg`` compressed TIFF files. A value from 0 (worst) to 100 (best) controls the image quality, similar to the JPEG -encoder. The default is 75. For example: - -.. code-block:: python +encoder. The default is 75. For example:: im.save("out.tif", compression="jpeg", quality=85) diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index 20a009cc1..0fb33de75 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -10,9 +10,7 @@ Text stroking ``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing operations. They allow text to be outlined, setting the width of the stroke and and the color respectively. If not provided, ``stroke_fill`` will default to -the ``fill`` parameter. - -.. code-block:: python +the ``fill`` parameter. :: from PIL import Image, ImageDraw, ImageFont @@ -28,9 +26,7 @@ the ``fill`` parameter. draw.multiline_text((10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0") -For example, - -.. code-block:: python +For example, :: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index 80002b0ce..f2e235289 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -118,9 +118,7 @@ Loading WMF images at a given DPI ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ On Windows, Pillow can read WMF files, with a default DPI of 72. An image can -now also be loaded at another resolution: - -.. code-block:: python +now also be loaded at another resolution:: from PIL import Image with Image.open("drawing.wmf") as im: @@ -136,16 +134,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call :py:meth:`~PIL.Image.Image.close` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index 0024a537d..cb46f127c 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -10,9 +10,7 @@ Allow saving of zero quality JPEG images If no quality was specified when saving a JPEG, Pillow internally used a value of zero to indicate that the default quality should be used. However, this removed the ability to actually save a JPEG with zero quality. This has now -been resolved. - -.. code-block:: python +been resolved. :: from PIL import Image im = Image.open("hopper.jpg") diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index c902ccf71..f11953168 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -76,9 +76,7 @@ ImageDraw.rounded_rectangle Added :py:meth:`~PIL.ImageDraw.ImageDraw.rounded_rectangle`. It works the same as :py:meth:`~PIL.ImageDraw.ImageDraw.rectangle`, except with an additional ``radius`` argument. ``radius`` is limited to half of the width or the height, so that users can -create a circle, but not any other ellipse. - -.. code-block:: python +create a circle, but not any other ellipse. :: from PIL import Image, ImageDraw im = Image.new("RGB", (200, 200)) diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst index 9becf9146..e61471e72 100644 --- a/docs/releasenotes/8.4.0.rst +++ b/docs/releasenotes/8.4.0.rst @@ -24,9 +24,7 @@ Added "transparency" argument for loading EPS images This new argument switches the Ghostscript device from "ppmraw" to "pngalpha", generating an RGBA image with a transparent background instead of an RGB image with a -white background. - -.. code-block:: python +white background. :: with Image.open("sample.eps") as im: im.load(transparency=True) diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index a19da361a..616cf4aa3 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -155,9 +155,7 @@ altered slightly with this change. Added support for pickling TrueType fonts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TrueType fonts may now be pickled and unpickled. For example: - -.. code-block:: python +TrueType fonts may now be pickled and unpickled. For example:: import pickle from PIL import ImageFont diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index e97b58a41..19690ca59 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -182,17 +182,13 @@ GifImagePlugin loading strategy Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as -well. - -.. code-block:: python +well. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS Or subsequent frames can be kept in ``P`` mode as long as there is only a single -palette. - -.. code-block:: python +palette. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 6dbfa2702..3dfb25840 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -59,9 +59,7 @@ Deprecated Use :py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -76,9 +74,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst new file mode 100644 index 000000000..df2ec53fa --- /dev/null +++ b/docs/releasenotes/9.5.0.rst @@ -0,0 +1,60 @@ +9.5.0 +----- + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +TODO + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Added ``dpi`` argument when saving PDFs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving a PDF, resolution could already be specified using the +``resolution`` argument. Now, a tuple of ``(x_resolution, y_resolution)`` can +be provided as ``dpi``. If both are provided, ``dpi`` will override +``resolution``. + +Added ``corners`` argument to ``ImageDraw.rounded_rectangle()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`.ImageDraw.rounded_rectangle` now accepts a keyword argument of +``corners``. This a tuple of Booleans, specifying whether to round each corner, +``(top_left, top_right, bottom_right, bottom_left)``. + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index a2b588696..177fb65dd 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 9.5.0 9.4.0 9.3.0 9.2.0 diff --git a/setup.py b/setup.py index 4382c1a97..d83439d7b 100755 --- a/setup.py +++ b/setup.py @@ -430,7 +430,6 @@ class pil_build_ext(build_ext): return sdk_path def build_extensions(self): - library_dirs = [] include_dirs = [] @@ -571,9 +570,7 @@ class pil_build_ext(build_ext): ): for dirname in _find_library_dirs_ldconfig(): _add_directory(library_dirs, dirname) - if sys.platform.startswith("linux") and os.environ.get( - "ANDROID_ROOT", None - ): + if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"): # termux support for android. # system libraries (zlib) are installed in /system/lib # headers are at $PREFIX/include @@ -917,7 +914,6 @@ class pil_build_ext(build_ext): self.summary_report(feature) def summary_report(self, feature): - print("-" * 68) print("PIL SETUP SUMMARY") print("-" * 68) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index e0dd4dede..075d46290 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -64,16 +64,27 @@ def bdf_char(f): bitmap.append(s[:-1]) bitmap = b"".join(bitmap) - [x, y, l, d] = [int(p) for p in props["BBX"].split()] - [dx, dy] = [int(p) for p in props["DWIDTH"].split()] + # The word BBX + # followed by the width in x (BBw), height in y (BBh), + # and x and y displacement (BBxoff0, BByoff0) + # of the lower left corner from the origin of the character. + width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] - bbox = (dx, dy), (l, -d - y, x + l, -d), (0, 0, x, y) + # The word DWIDTH + # followed by the width in x and y of the character in device pixels. + dwx, dwy = [int(p) for p in props["DWIDTH"].split()] + + bbox = ( + (dwx, dwy), + (x_disp, -y_disp - height, width + x_disp, -y_disp), + (0, 0, width, height), + ) try: - im = Image.frombytes("1", (x, y), bitmap, "hex", "1") + im = Image.frombytes("1", (width, height), bitmap, "hex", "1") except ValueError: # deal with zero-width characters - im = Image.new("1", (x, y)) + im = Image.new("1", (width, height)) return id, int(props["ENCODING"]), bbox, im diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index e13b18f27..5bda0a5b0 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -223,7 +223,6 @@ class BmpImageFile(ImageFile.ImageFile): # --------------- Once the header is processed, process the palette/LUT if self.mode == "P": # Paletted for 1, 4 and 8 bit images - # ---------------------------------------------------- 1-bit images if not (0 < file_info["colors"] <= 65536): msg = f"Unsupported BMP Palette size ({file_info['colors']})" @@ -360,7 +359,6 @@ class BmpRleDecoder(ImageFile.PyDecoder): # Image plugin for the DIB format (BMP alias) # ============================================================================= class DibImageFile(BmpImageFile): - format = "DIB" format_description = "Windows Bitmap" diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index a0da1b786..0425bbd75 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class BufrStubImageFile(ImageFile.StubImageFile): - format = "BUFR" format_description = "BUFR" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(4)): diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index aedc6ce7f..94efff341 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -32,12 +32,10 @@ def _accept(prefix): class CurImageFile(BmpImagePlugin.BmpImageFile): - format = "CUR" format_description = "Windows Cursor" def _open(self): - offset = self.fp.tell() # check magic diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index 81c0314f0..cde9d42f0 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -37,13 +37,11 @@ def _accept(prefix): class DcxImageFile(PcxImageFile): - format = "DCX" format_description = "Intel DCX" _close_exclusive_fp_after_loading = False def _open(self): - # Header s = self.fp.read(4) if not _accept(s): diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index dd68c13e5..60cb46df9 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -286,7 +286,6 @@ class EpsImageFile(ImageFile.ImageFile): # Scan for an "ImageData" descriptor while s[:1] == "%": - if len(s) > 255: msg = "not an EPS file" raise SyntaxError(msg) @@ -317,7 +316,6 @@ class EpsImageFile(ImageFile.ImageFile): raise OSError(msg) def _find_offset(self, fp): - s = fp.read(4) if s == b"%!PS": diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 536bc1fe6..1359aeb12 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -19,7 +19,6 @@ def _accept(prefix): class FitsImageFile(ImageFile.ImageFile): - format = "FITS" format_description = "FITS" @@ -33,7 +32,7 @@ class FitsImageFile(ImageFile.ImageFile): keyword = header[:8].strip() if keyword == b"END": break - value = header[8:].strip() + value = header[8:].split(b"/")[0].strip() if value.startswith(b"="): value = value[1:].strip() if not headers and (not _accept(keyword) or value != b"T"): diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py index 86eb2d5a2..50948ec42 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -44,7 +44,6 @@ def register_handler(handler): class FITSStubImageFile(ImageFile.StubImageFile): - format = FitsImagePlugin.FitsImageFile.format format_description = FitsImagePlugin.FitsImageFile.format_description diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 66681939d..f4e89a03e 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -40,13 +40,11 @@ def _accept(prefix): class FliImageFile(ImageFile.ImageFile): - format = "FLI" format_description = "Autodesk FLI/FLC Animation" _close_exclusive_fp_after_loading = False def _open(self): - # HEAD s = self.fp.read(128) if not (_accept(s) and s[20:22] == b"\x00\x00"): diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index c5fc80b37..5ec0a6632 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -36,7 +36,6 @@ class FontFile: bitmap = None def __init__(self): - self.info = {} self.glyph = [None] * 256 diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 8ddc6b40b..2450c67e9 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -48,7 +48,6 @@ def _accept(prefix): class FpxImageFile(ImageFile.ImageFile): - format = "FPX" format_description = "FlashPix" @@ -157,7 +156,6 @@ class FpxImageFile(ImageFile.ImageFile): self.tile = [] for i in range(0, len(s), length): - x1 = min(xsize, x + xtile) y1 = min(ysize, y + ytile) @@ -174,7 +172,6 @@ class FpxImageFile(ImageFile.ImageFile): ) elif compression == 1: - # FIXME: the fill decoder is not implemented self.tile.append( ( @@ -186,7 +183,6 @@ class FpxImageFile(ImageFile.ImageFile): ) elif compression == 2: - internal_color_conversion = s[14] jpeg_tables = s[15] rawmode = self.rawmode @@ -234,12 +230,19 @@ class FpxImageFile(ImageFile.ImageFile): self.fp = None def load(self): - if not self.fp: self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) return ImageFile.ImageFile.load(self) + def close(self): + self.ole.close() + super().close() + + def __exit__(self, *args): + self.ole.close() + super().__exit__() + # # -------------------------------------------------------------------- diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 828a45ced..994a6e8eb 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -37,7 +37,6 @@ def _accept(prefix): class GbrImageFile(ImageFile.ImageFile): - format = "GBR" format_description = "GIMP brush file" diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 3875dc866..7dda4f143 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -44,7 +44,6 @@ class GdImageFile(ImageFile.ImageFile): format_description = "GD uncompressed images" def _open(self): - # Header s = self.fp.read(1037) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 6ee1bd3d8..eadee1560 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -61,7 +61,6 @@ def _accept(prefix): class GifImageFile(ImageFile.ImageFile): - format = "GIF" format_description = "Compuserve GIF" _close_exclusive_fp_after_loading = False @@ -81,7 +80,6 @@ class GifImageFile(ImageFile.ImageFile): return False def _open(self): - # Screen s = self.fp.read(13) if not _accept(s): @@ -157,7 +155,6 @@ class GifImageFile(ImageFile.ImageFile): raise EOFError(msg) from e def _seek(self, frame, update_image=True): - if frame == 0: # rewind self.__offset = 0 @@ -195,7 +192,6 @@ class GifImageFile(ImageFile.ImageFile): interlace = None frame_dispose_extent = None while True: - if not s: s = self.fp.read(1) if not s or s == b";": @@ -579,7 +575,6 @@ def _getbbox(base_im, im_frame): def _write_multiple_frames(im, fp, palette): - duration = im.encoderinfo.get("duration") disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) @@ -752,7 +747,6 @@ def _write_local_header(fp, im, offset, flags): def _save_netpbm(im, fp, filename): - # Unused by default. # To use, uncomment the register_save call at the end of the file. # diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index b5c5e3ca4..8e801be0b 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -64,18 +64,15 @@ SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] class GradientFile: - gradient = None def getpalette(self, entries=256): - palette = [] ix = 0 x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] for i in range(entries): - x = i / (entries - 1) while x1 < x: @@ -105,7 +102,6 @@ class GimpGradientFile(GradientFile): """File handler for GIMP's gradient format.""" def __init__(self, fp): - if fp.readline()[:13] != b"GIMP Gradient": msg = "not a GIMP gradient file" raise SyntaxError(msg) @@ -121,7 +117,6 @@ class GimpGradientFile(GradientFile): gradient = [] for i in range(count): - s = fp.readline().split() w = [float(x) for x in s[:11]] diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 2e9cbe58d..d38892894 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -25,7 +25,6 @@ class GimpPaletteFile: rawmode = "RGB" def __init__(self, fp): - self.palette = [o8(i) * 3 for i in range(256)] if fp.readline()[:12] != b"GIMP Palette": @@ -33,7 +32,6 @@ class GimpPaletteFile: raise SyntaxError(msg) for i in range(256): - s = fp.readline() if not s: break @@ -55,5 +53,4 @@ class GimpPaletteFile: self.palette = b"".join(self.palette) def getpalette(self): - return self.palette, self.rawmode diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 2088eb7b0..8a799f19c 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class GribStubImageFile(ImageFile.StubImageFile): - format = "GRIB" format_description = "GRIB" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index d6f283739..bba05ed65 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class HDF5StubImageFile(ImageFile.StubImageFile): - format = "HDF5" format_description = "HDF5" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index e76d0c35a..c2f050edd 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -135,7 +135,6 @@ def read_png_or_jpeg2000(fobj, start_length, size): class IcnsFile: - SIZES = { (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)], @@ -189,7 +188,7 @@ class IcnsFile: def itersizes(self): sizes = [] for size, fmts in self.SIZES.items(): - for (fmt, reader) in fmts: + for fmt, reader in fmts: if fmt in self.dct: sizes.append(size) break diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 568e6d38d..a188f8fdc 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -185,7 +185,7 @@ class IcoFile: return {(h["width"], h["height"]) for h in self.entry} def getentryindex(self, size, bpp=False): - for (i, h) in enumerate(self.entry): + for i, h in enumerate(self.entry): if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): return i return 0 diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 875a20326..746743f65 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -115,13 +115,11 @@ def number(s): class ImImageFile(ImageFile.ImageFile): - format = "IM" format_description = "IFUNC Image Memory" _close_exclusive_fp_after_loading = False def _open(self): - # Quick rejection: if there's not an LF among the first # 100 bytes, this is (probably) not a text header. @@ -140,7 +138,6 @@ class ImImageFile(ImageFile.ImageFile): self.rawmode = "L" while True: - s = self.fp.read(1) # Some versions of IFUNC uses \n\r instead of \r\n... @@ -169,7 +166,6 @@ class ImImageFile(ImageFile.ImageFile): raise SyntaxError(msg) from e if m: - k, v = m.group(1, 2) # Don't know if this is the correct encoding, @@ -200,7 +196,6 @@ class ImImageFile(ImageFile.ImageFile): n += 1 else: - msg = "Syntax error in IM header: " + s.decode("ascii", "replace") raise SyntaxError(msg) @@ -252,7 +247,6 @@ class ImImageFile(ImageFile.ImageFile): self._fp = self.fp # FIXME: hack if self.rawmode[:2] == "F;": - # ifunc95 formats try: # use bit decoder (if necessary) @@ -332,7 +326,6 @@ SAVE = { def _save(im, fp, filename): - try: image_type, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ad0d25add..cf9ab2df6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -153,6 +153,7 @@ def isImageType(t): # # Constants + # transpose class Transpose(IntEnum): FLIP_LEFT_RIGHT = 0 @@ -391,7 +392,6 @@ def init(): def _getdecoder(mode, decoder_name, args, extra=()): - # tweak arguments if args is None: args = () @@ -415,7 +415,6 @@ def _getdecoder(mode, decoder_name, args, extra=()): def _getencoder(mode, encoder_name, args, extra=()): - # tweak arguments if args is None: args = () @@ -766,17 +765,17 @@ class Image: bufsize = max(65536, self.size[0] * 4) # see RawEncode.c - data = [] + output = [] while True: - l, s, d = e.encode(bufsize) - data.append(d) - if s: + bytes_consumed, errcode, data = e.encode(bufsize) + output.append(data) + if errcode: break - if s < 0: - msg = f"encoder error {s} in tobytes" + if errcode < 0: + msg = f"encoder error {errcode} in tobytes" raise RuntimeError(msg) - return b"".join(data) + return b"".join(output) def tobitmap(self, name="image"): """ @@ -1433,6 +1432,11 @@ class Image: return {get_name(root.tag): get_value(root)} def getexif(self): + """ + Gets EXIF data from the image. + + :returns: an :py:class:`~PIL.Image.Exif` object. + """ if self._exif is None: self._exif = Exif() self._exif._loaded = False @@ -3602,6 +3606,39 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): + """ + This class provides read and write access to EXIF image data:: + + from PIL import Image + im = Image.open("exif.png") + exif = im.getexif() # Returns an instance of this class + + Information can be read and written, iterated over or deleted:: + + print(exif[274]) # 1 + exif[274] = 2 + for k, v in exif.items(): + print("Tag", k, "Value", v) # Tag 274 Value 2 + del exif[274] + + To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd` + returns a dictionary:: + + from PIL import ExifTags + im = Image.open("exif_gps.jpg") + exif = im.getexif() + gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) + print(gps_ifd) + + Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, + ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. + + :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: + + print(exif[ExifTags.Base.Software]) # PIL + print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 + """ + endian = None bigtiff = False diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py index fec4694b2..701200317 100644 --- a/src/PIL/ImageChops.py +++ b/src/PIL/ImageChops.py @@ -38,9 +38,7 @@ def duplicate(image): def invert(image): """ - Invert an image (channel). - - .. code-block:: python + Invert an image (channel). :: out = MAX - image @@ -54,9 +52,7 @@ def invert(image): def lighter(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the lighter values. - - .. code-block:: python + the lighter values. :: out = max(image1, image2) @@ -71,9 +67,7 @@ def lighter(image1, image2): def darker(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the darker values. - - .. code-block:: python + the darker values. :: out = min(image1, image2) @@ -88,9 +82,7 @@ def darker(image1, image2): def difference(image1, image2): """ Returns the absolute value of the pixel-by-pixel difference between the two - images. - - .. code-block:: python + images. :: out = abs(image1 - image2) @@ -107,9 +99,7 @@ def multiply(image1, image2): Superimposes two images on top of each other. If you multiply an image with a solid black image, the result is black. If - you multiply with a solid white image, the image is unaffected. - - .. code-block:: python + you multiply with a solid white image, the image is unaffected. :: out = image1 * image2 / MAX @@ -123,9 +113,7 @@ def multiply(image1, image2): def screen(image1, image2): """ - Superimposes two inverted images on top of each other. - - .. code-block:: python + Superimposes two inverted images on top of each other. :: out = MAX - ((MAX - image1) * (MAX - image2) / MAX) @@ -176,9 +164,7 @@ def overlay(image1, image2): def add(image1, image2, scale=1.0, offset=0): """ Adds two images, dividing the result by scale and adding the - offset. If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + offset. If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 + image2) / scale + offset) @@ -193,9 +179,7 @@ def add(image1, image2, scale=1.0, offset=0): def subtract(image1, image2, scale=1.0, offset=0): """ Subtracts two images, dividing the result by scale and adding the offset. - If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 - image2) / scale + offset) @@ -208,9 +192,7 @@ def subtract(image1, image2, scale=1.0, offset=0): def add_modulo(image1, image2): - """Add two images, without clipping the result. - - .. code-block:: python + """Add two images, without clipping the result. :: out = ((image1 + image2) % MAX) @@ -223,9 +205,7 @@ def add_modulo(image1, image2): def subtract_modulo(image1, image2): - """Subtract two images, without clipping the result. - - .. code-block:: python + """Subtract two images, without clipping the result. :: out = ((image1 - image2) % MAX) @@ -243,9 +223,7 @@ def logical_and(image1, image2): Both of the images must have mode "1". If you would like to perform a logical AND on an image with a mode other than "1", try :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask - as the second image. - - .. code-block:: python + as the second image. :: out = ((image1 and image2) % MAX) @@ -260,9 +238,7 @@ def logical_and(image1, image2): def logical_or(image1, image2): """Logical OR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((image1 or image2) % MAX) @@ -277,9 +253,7 @@ def logical_or(image1, image2): def logical_xor(image1, image2): """Logical XOR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((bool(image1) != bool(image2)) % MAX) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index ce29a163b..5a0df09cb 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -295,29 +295,43 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_rectangle(xy, ink, 0, width) - def rounded_rectangle(self, xy, radius=0, fill=None, outline=None, width=1): + def rounded_rectangle( + self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None + ): """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): (x0, y0), (x1, y1) = xy else: x0, y0, x1, y1 = xy + if x1 < x0: + msg = "x1 must be greater than or equal to x0" + raise ValueError(msg) + if y1 < y0: + msg = "y1 must be greater than or equal to y0" + raise ValueError(msg) + if corners is None: + corners = (True, True, True, True) d = radius * 2 - full_x = d >= x1 - x0 - if full_x: - # The two left and two right corners are joined - d = x1 - x0 - full_y = d >= y1 - y0 - if full_y: - # The two top and two bottom corners are joined - d = y1 - y0 - if full_x and full_y: - # If all corners are joined, that is a circle - return self.ellipse(xy, fill, outline, width) + full_x, full_y = False, False + if all(corners): + full_x = d >= x1 - x0 + if full_x: + # The two left and two right corners are joined + d = x1 - x0 + full_y = d >= y1 - y0 + if full_y: + # The two top and two bottom corners are joined + d = y1 - y0 + if full_x and full_y: + # If all corners are joined, that is a circle + return self.ellipse(xy, fill, outline, width) - if d == 0: - # If the corners have no curve, that is a rectangle + if d == 0 or not any(corners): + # If the corners have no curve, + # or there are no corners, + # that is a rectangle return self.rectangle(xy, fill, outline, width) r = d // 2 @@ -338,12 +352,17 @@ class ImageDraw: ) else: # Draw four separate corners - parts = ( - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), - ((x0, y0, x0 + d, y0 + d), 180, 270), - ) + parts = [] + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) + ): + if corners[i]: + parts.append(part) for part in parts: if pieslice: self.draw.draw_pieslice(*(part + (fill, 1))) @@ -358,25 +377,50 @@ class ImageDraw: else: self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) if not full_x and not full_y: - self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1) - self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1) + left = [x0, y0, x0 + r, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, fill, 1) + + right = [x1 - r, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, fill, 1) if ink is not None and ink != fill and width != 0: draw_corners(False) if not full_x: - self.draw.draw_rectangle( - (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1 - ) + top = [x0, y0, x1, y0 + width - 1] + if corners[0]: + top[0] += r + 1 + if corners[1]: + top[2] -= r + 1 + self.draw.draw_rectangle(top, ink, 1) + + bottom = [x0, y1 - width + 1, x1, y1] + if corners[3]: + bottom[0] += r + 1 + if corners[2]: + bottom[2] -= r + 1 + self.draw.draw_rectangle(bottom, ink, 1) if not full_y: - self.draw.draw_rectangle( - (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1 - ) + left = [x0, y0, x0 + width - 1, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, ink, 1) + + right = [x1 - width + 1, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, ink, 1) def _multiline_check(self, text): """Draw text.""" @@ -928,8 +972,8 @@ def floodfill(image, xy, value, border=None, thresh=0): full_edge = set() while edge: new_edge = set() - for (x, y) in edge: # 4 adjacent method - for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + for x, y in edge: # 4 adjacent method + for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): # If already processed, or if a coordinate is negative, skip if (s, t) in full_edge or s < 0 or t < 0: continue diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 12391955f..8e4f7dfb2 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -395,7 +395,6 @@ class Parser: # parse what we have if self.decoder: - if self.offset > 0: # skip header skip = min(len(self.data), self.offset) @@ -420,14 +419,12 @@ class Parser: self.data = self.data[n:] elif self.image: - # if we end up here with no decoder, this file cannot # be incrementally parsed. wait until we've gotten all # available data pass else: - # attempt to open this file try: with io.BytesIO(self.data) as fp: @@ -533,20 +530,20 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): encoder.setimage(im.im, b) if encoder.pushes_fd: encoder.setfd(fp) - l, s = encoder.encode_to_pyfd() + errcode = encoder.encode_to_pyfd()[1] else: if exc: # compress to Python file-compatible object while True: - l, s, d = encoder.encode(bufsize) - fp.write(d) - if s: + errcode, data = encoder.encode(bufsize)[1:] + fp.write(data) + if errcode: break else: # slight speedup: compress to real file object - s = encoder.encode_to_file(fh, bufsize) - if s < 0: - msg = f"encoder error {s} when writing image file" + errcode = encoder.encode_to_file(fh, bufsize) + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) from exc finally: encoder.cleanup() diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b144c3dd2..30f6694e6 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -90,7 +90,6 @@ class ImageFont: """PIL font wrapper""" def _load_pilfont(self, filename): - with open(filename, "rb") as fp: image = None for ext in (".png", ".gif", ".pbm"): @@ -116,7 +115,6 @@ class ImageFont: image.close() def _load_pilfont_data(self, file, image): - # read PILfont header if file.readline() != b"PILfont\n": msg = "Not a PILfont file" @@ -299,27 +297,21 @@ class FreeTypeFont: string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = font.getlength("Hello") world = font.getlength("World") hello_world = hello + world # not adjusted for kerning assert hello_world == font.getlength("HelloWorld") # may fail - use - - .. code-block:: python + use :: hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning world = font.getlength("World") hello_world = hello + world # adjusted for kerning assert hello_world == font.getlength("HelloWorld") # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) @@ -1020,7 +1012,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): if windir: dirs.append(os.path.join(windir, "fonts")) elif sys.platform in ("linux", "linux2"): - lindirs = os.environ.get("XDG_DATA_DIRS", "") + lindirs = os.environ.get("XDG_DATA_DIRS") if not lindirs: # According to the freedesktop spec, XDG_DATA_DIRS should # default to /usr/share diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 16c83f4e4..301c593c7 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -205,7 +205,6 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the mapping (2-color) if mid is None: - range_map = range(0, whitepoint - blackpoint) for i in range_map: @@ -215,7 +214,6 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the mapping (3-color) else: - range_map1 = range(0, midpoint - blackpoint) range_map2 = range(0, whitepoint - midpoint) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index fe0d32155..e455c0459 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -248,11 +248,9 @@ def wedge(mode="RGB"): def load(filename): - # FIXME: supports GIMP gradients only with open(filename, "rb") as fp: - for paletteHandler in [ GimpPaletteFile.GimpPaletteFile, GimpGradientFile.GimpGradientFile, diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 29d900bef..f0e73fb90 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -390,7 +390,6 @@ else: if __name__ == "__main__": - if len(sys.argv) < 2: print("Syntax: python3 ImageShow.py imagefile [title]") sys.exit() diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 09a6356fa..ef569ed2e 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -97,7 +97,6 @@ class PhotoImage: """ def __init__(self, image=None, size=None, **kw): - # Tk compatibility: file or data if image is None: image = _get_image_from_kw(kw) @@ -209,7 +208,6 @@ class BitmapImage: """ def __init__(self, image=None, **kw): - # Tk compatibility: file or data if image is None: image = _get_image_from_kw(kw) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index cfeadd53c..ac267457b 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -30,12 +30,10 @@ field = re.compile(rb"([a-z]*) ([^ \r\n]*)") class ImtImageFile(ImageFile.ImageFile): - format = "IMT" format_description = "IM Tools" def _open(self): - # Quick rejection: if there's not a LF among the first # 100 bytes, this is (probably) not a text header. @@ -47,7 +45,6 @@ class ImtImageFile(ImageFile.ImageFile): xsize = ysize = 0 while True: - if buffer: s = buffer[:1] buffer = buffer[1:] @@ -57,7 +54,6 @@ class ImtImageFile(ImageFile.ImageFile): break if s == b"\x0C": - # image data begins self.tile = [ ( @@ -71,7 +67,6 @@ class ImtImageFile(ImageFile.ImageFile): break else: - # read key/value pair if b"\n" not in buffer: buffer += self.fp.read(100) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 774817569..4c47b55c1 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -48,7 +48,6 @@ def dump(c): class IptcImageFile(ImageFile.ImageFile): - format = "IPTC" format_description = "IPTC/NAA" @@ -84,7 +83,6 @@ class IptcImageFile(ImageFile.ImageFile): return tag, size def _open(self): - # load descriptive fields while True: offset = self.fp.tell() @@ -134,7 +132,6 @@ class IptcImageFile(ImageFile.ImageFile): ] def load(self): - if len(self.tile) != 1 or self.tile[0][0] != "iptc": return ImageFile.ImageFile.load(self) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index b9c80236e..71ae84c04 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -344,12 +344,10 @@ def _accept(prefix): class JpegImageFile(ImageFile.ImageFile): - format = "JPEG" format_description = "JPEG (ISO 10918)" def _open(self): - s = self.fp.read(3) if not _accept(s): @@ -370,7 +368,6 @@ class JpegImageFile(ImageFile.ImageFile): self.icclist = [] while True: - i = s[0] if i == 0xFF: s = s + self.fp.read(1) @@ -418,7 +415,6 @@ class JpegImageFile(ImageFile.ImageFile): return s def draft(self, mode, size): - if len(self.tile) != 1: return @@ -455,7 +451,6 @@ class JpegImageFile(ImageFile.ImageFile): return self.mode, box def load_djpeg(self): - # ALTERNATIVE: handle JPEGs via the IJG command line utilities f, path = tempfile.mkstemp() @@ -735,10 +730,10 @@ def _save(im, fp, filename): extra = info.get("extra", b"") + MAX_BYTES_IN_MARKER = 65533 icc_profile = info.get("icc_profile") if icc_profile: ICC_OVERHEAD_LEN = 14 - MAX_BYTES_IN_MARKER = 65533 MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN markers = [] while icc_profile: @@ -769,6 +764,9 @@ def _save(im, fp, filename): exif = info.get("exif", b"") if isinstance(exif, Image.Exif): exif = exif.tobytes() + if len(exif) > MAX_BYTES_IN_MARKER: + msg = "EXIF data is too long" + raise ValueError(msg) # get keyword arguments im.encoderconfig = ( diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 8d4d826aa..17c008b9a 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -30,12 +30,10 @@ def _accept(s): class McIdasImageFile(ImageFile.ImageFile): - format = "MCIDAS" format_description = "McIdas area file" def _open(self): - # parse area file directory s = self.fp.read(256) if not _accept(s) or len(s) != 256: diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index e7e1054a3..58f7327bd 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -34,13 +34,11 @@ def _accept(prefix): class MicImageFile(TiffImagePlugin.TiffImageFile): - format = "MIC" format_description = "Microsoft Image Composer" _close_exclusive_fp_after_loading = False def _open(self): - # read the OLE directory and see if this is a likely # to be a Microsoft Image Composer file @@ -91,6 +89,14 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): def tell(self): return self.frame + def close(self): + self.ole.close() + super().close() + + def __exit__(self, *args): + self.ole.close() + super().__exit__() + # # -------------------------------------------------------------------- diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index 2d799d6d8..d96d3a11c 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -58,12 +58,10 @@ class BitStream: class MpegImageFile(ImageFile.ImageFile): - format = "MPEG" format_description = "MPEG" def _open(self): - s = BitStream(self.fp) if s.read(32) != 0x1B3: diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index b1ec2c7bc..f9261c77d 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -101,7 +101,6 @@ def _save_all(im, fp, filename): class MpoImageFile(JpegImagePlugin.JpegImageFile): - format = "MPO" format_description = "MPO (CIPA DC-007)" _close_exclusive_fp_after_loading = False diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 5420894dc..c6567b2ae 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -44,12 +44,10 @@ def _accept(prefix): class MspImageFile(ImageFile.ImageFile): - format = "MSP" format_description = "Windows Paint" def _open(self): - # Header s = self.fp.read(32) if not _accept(s): @@ -111,7 +109,6 @@ class MspDecoder(ImageFile.PyDecoder): _pulls_fd = True def decode(self, buffer): - img = io.BytesIO() blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) try: @@ -162,7 +159,6 @@ Image.register_decoder("MSP", MspDecoder) def _save(im, fp, filename): - if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 07acd5580..4a2c497fc 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -22,11 +22,9 @@ class PaletteFile: rawmode = "RGB" def __init__(self, fp): - self.palette = [(i, i, i) for i in range(256)] while True: - s = fp.readline() if not s: @@ -50,5 +48,4 @@ class PaletteFile: self.palette = b"".join(self.palette) def getpalette(self): - return self.palette, self.rawmode diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 109aad9ab..a88a90791 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -112,9 +112,7 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} def _save(im, fp, filename): - if im.mode == "P": - # we assume this is a color Palm image with the standard colormap, # unless the "info" dict has a "custom-colormap" field @@ -147,14 +145,12 @@ def _save(im, fp, filename): version = 1 elif im.mode == "1": - # monochrome -- write it inverted, as is the Palm standard rawmode = "1;I" bpp = 1 version = 0 else: - msg = f"cannot write mode {im.mode} as Palm" raise OSError(msg) diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 5802d386a..e390f3fe5 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -24,12 +24,10 @@ from . import Image, ImageFile class PcdImageFile(ImageFile.ImageFile): - format = "PCD" format_description = "Kodak PhotoCD" def _open(self): - # rough self.fp.seek(2048) s = self.fp.read(2048) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index ecce1b097..8db5822fe 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -58,7 +58,6 @@ class PcfFontFile(FontFile.FontFile): name = "name" def __init__(self, fp, charset_encoding="iso8859-1"): - self.charset_encoding = charset_encoding magic = l32(fp.read(4)) @@ -87,12 +86,24 @@ class PcfFontFile(FontFile.FontFile): for ch, ix in enumerate(encoding): if ix is not None: - x, y, l, r, w, a, d, f = metrics[ix] - glyph = (w, 0), (l, d - y, x + l, d), (0, 0, x, y), bitmaps[ix] - self.glyph[ch] = glyph + ( + xsize, + ysize, + left, + right, + width, + ascent, + descent, + attributes, + ) = metrics[ix] + self.glyph[ch] = ( + (width, 0), + (left, descent - ysize, xsize + left, descent), + (0, 0, xsize, ysize), + bitmaps[ix], + ) def _getformat(self, tag): - format, size, offset = self.toc[tag] fp = self.fp @@ -108,7 +119,6 @@ class PcfFontFile(FontFile.FontFile): return fp, format, i16, i32 def _load_properties(self): - # # font properties @@ -136,7 +146,6 @@ class PcfFontFile(FontFile.FontFile): return properties def _load_metrics(self): - # # font metrics @@ -147,7 +156,6 @@ class PcfFontFile(FontFile.FontFile): append = metrics.append if (format & 0xFF00) == 0x100: - # "compressed" metrics for i in range(i16(fp.read(2))): left = i8(fp.read(1)) - 128 @@ -160,7 +168,6 @@ class PcfFontFile(FontFile.FontFile): append((xsize, ysize, left, right, width, ascent, descent, 0)) else: - # "jumbo" metrics for i in range(i32(fp.read(4))): left = i16(fp.read(2)) @@ -176,7 +183,6 @@ class PcfFontFile(FontFile.FontFile): return metrics def _load_bitmaps(self, metrics): - # # bitmap data @@ -213,9 +219,11 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - x, y, l, r, w, a, d, f = metrics[i] - b, e = offsets[i], offsets[i + 1] - bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x))) + xsize, ysize = metrics[i][:2] + b, e = offsets[i : i + 2] + bitmaps.append( + Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize)) + ) return bitmaps diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 3202475dc..f42c2456b 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -45,12 +45,10 @@ def _accept(prefix): class PcxImageFile(ImageFile.ImageFile): - format = "PCX" format_description = "Paintbrush" def _open(self): - # header s = self.fp.read(128) if not _accept(s): @@ -143,7 +141,6 @@ SAVE = { def _save(im, fp, filename): - try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 0b2938d6f..c41f8aee0 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -53,7 +53,12 @@ def _save(im, fp, filename, save_all=False): else: existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") - resolution = im.encoderinfo.get("resolution", 72.0) + dpi = im.encoderinfo.get("dpi") + if dpi: + x_resolution = dpi[0] + y_resolution = dpi[1] + else: + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) info = { "title": None @@ -220,8 +225,8 @@ def _save(im, fp, filename, save_all=False): stream=stream, Type=PdfParser.PdfName("XObject"), Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / resolution, - Height=height, # * 72.0 / resolution, + Width=width, # * 72.0 / x_resolution, + Height=height, # * 72.0 / y_resolution, Filter=filter, BitsPerComponent=bits, Decode=decode, @@ -241,8 +246,8 @@ def _save(im, fp, filename, save_all=False): MediaBox=[ 0, 0, - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ], Contents=contents_refs[page_number], ) @@ -251,8 +256,8 @@ def _save(im, fp, filename, save_all=False): # page contents page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ) existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 8d0a34dba..7eb82228a 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -35,12 +35,10 @@ def _accept(prefix): class PixarImageFile(ImageFile.ImageFile): - format = "PIXAR" format_description = "PIXAR raster image" def _open(self): - # assuming a 4-byte magic label s = self.fp.read(4) if not _accept(s): diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index b6626bbc5..9078957dc 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -161,7 +161,6 @@ def _crc32(data, seed=0): class ChunkStream: def __init__(self, fp): - self.fp = fp self.queue = [] @@ -195,7 +194,6 @@ class ChunkStream: self.queue = self.fp = None def push(self, cid, pos, length): - self.queue.append((cid, pos, length)) def call(self, cid, pos, length): @@ -230,7 +228,6 @@ class ChunkStream: self.fp.read(4) def verify(self, endchunk=b"IEND"): - # Simple approach; just calculate checksum for all remaining # blocks. Must be called directly after open. @@ -397,7 +394,6 @@ class PngStream(ChunkStream): self._seq_num = self.rewind_state["seq_num"] def chunk_iCCP(self, pos, length): - # ICC profile s = ImageFile._safe_read(self.fp, length) # according to PNG spec, the iCCP chunk contains: @@ -425,7 +421,6 @@ class PngStream(ChunkStream): return s def chunk_IHDR(self, pos, length): - # image header s = ImageFile._safe_read(self.fp, length) if length < 13: @@ -446,7 +441,6 @@ class PngStream(ChunkStream): return s def chunk_IDAT(self, pos, length): - # image data if "bbox" in self.im_info: tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] @@ -459,12 +453,10 @@ class PngStream(ChunkStream): raise EOFError def chunk_IEND(self, pos, length): - # end of PNG image raise EOFError def chunk_PLTE(self, pos, length): - # palette s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": @@ -472,7 +464,6 @@ class PngStream(ChunkStream): return s def chunk_tRNS(self, pos, length): - # transparency s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": @@ -524,7 +515,6 @@ class PngStream(ChunkStream): return s def chunk_pHYs(self, pos, length): - # pixels per unit s = ImageFile._safe_read(self.fp, length) if length < 9: @@ -542,7 +532,6 @@ class PngStream(ChunkStream): return s def chunk_tEXt(self, pos, length): - # text s = ImageFile._safe_read(self.fp, length) try: @@ -562,7 +551,6 @@ class PngStream(ChunkStream): return s def chunk_zTXt(self, pos, length): - # compressed text s = ImageFile._safe_read(self.fp, length) try: @@ -597,7 +585,6 @@ class PngStream(ChunkStream): return s def chunk_iTXt(self, pos, length): - # international text r = s = ImageFile._safe_read(self.fp, length) try: @@ -721,12 +708,10 @@ def _accept(prefix): class PngImageFile(ImageFile.ImageFile): - format = "PNG" format_description = "Portable network graphics" def _open(self): - if not _accept(self.fp.read(8)): msg = "not a PNG file" raise SyntaxError(msg) @@ -740,7 +725,6 @@ class PngImageFile(ImageFile.ImageFile): self.png = PngStream(self.fp) while True: - # # get next chunk @@ -1264,7 +1248,6 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): mode = im.mode if mode == "P": - # # attempt to minimize storage requirements for palette images if "bits" in im.encoderinfo: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index dee2f1e15..5aa418044 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -51,7 +51,6 @@ def _accept(prefix): class PpmImageFile(ImageFile.ImageFile): - format = "PPM" format_description = "Pbmplus image" diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 7e8d12759..5a5d60d56 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -51,13 +51,11 @@ def _accept(prefix): class PsdImageFile(ImageFile.ImageFile): - format = "PSD" format_description = "Adobe Photoshop" _close_exclusive_fp_after_loading = False def _open(self): - read = self.fp.read # @@ -177,7 +175,6 @@ def _layerinfo(fp, ct_bytes): raise SyntaxError(msg) for _ in range(abs(ct)): - # bounding box y0 = i32(read(4)) x0 = i32(read(4)) @@ -250,7 +247,6 @@ def _layerinfo(fp, ct_bytes): def _maketile(file, mode, bbox, channels): - tile = None read = file.read diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index d533c55e5..3662ffd15 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -49,12 +49,10 @@ MODES = { ## # Image plugin for SGI images. class SgiImageFile(ImageFile.ImageFile): - format = "SGI" format_description = "SGI Image File Format" def _open(self): - # HEAD headlen = 512 s = self.fp.read(headlen) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 1192c2d73..eac27e679 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -91,7 +91,6 @@ def isSpiderImage(filename): class SpiderImageFile(ImageFile.ImageFile): - format = "SPIDER" format_description = "Spider 2D image" _close_exclusive_fp_after_loading = False @@ -200,6 +199,7 @@ class SpiderImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # Image series + # given a list of filenames, return a list of images def loadImageSeries(filelist=None): """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" @@ -289,7 +289,6 @@ Image.register_open(SpiderImageFile.format, SpiderImageFile) Image.register_save(SpiderImageFile.format, _save_spider) if __name__ == "__main__": - if len(sys.argv) < 2: print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") sys.exit() diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index c64de4444..6712583d7 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -30,12 +30,10 @@ def _accept(prefix): class SunImageFile(ImageFile.ImageFile): - format = "SUN" format_description = "Sun Raster File" def _open(self): - # The Sun Raster file header is 32 bytes in length # and has the following format: diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 20e8a083f..32928f6af 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -32,7 +32,6 @@ class TarIO(ContainerIO.ContainerIO): self.fh = open(tarfile, "rb") while True: - s = self.fh.read(512) if len(s) != 512: msg = "unexpected end of tar file" diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 53fe6ef5c..67dfc3d3c 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -46,12 +46,10 @@ MODES = { class TgaImageFile(ImageFile.ImageFile): - format = "TGA" format_description = "Targa" def _open(self): - # process header s = self.fp.read(18) @@ -174,7 +172,6 @@ SAVE = { def _save(im, fp, filename): - try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index baa9abad8..8c0431492 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -425,6 +425,9 @@ class IFDRational(Rational): __ceil__ = _delegate("__ceil__") __floor__ = _delegate("__floor__") __round__ = _delegate("__round__") + # Python >= 3.11 + if hasattr(Fraction, "__int__"): + __int__ = _delegate("__int__") class ImageFileDirectory_v2(MutableMapping): @@ -764,6 +767,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(7) def write_undefined(self, value): + if isinstance(value, int): + value = str(value).encode("ascii", "replace") return value @_register_loader(10, 8) @@ -793,7 +798,6 @@ class ImageFileDirectory_v2(MutableMapping): return ret def load(self, fp): - self.reset() self._offset = fp.tell() @@ -938,7 +942,6 @@ class ImageFileDirectory_v2(MutableMapping): return result def save(self, fp): - if fp.tell() == 0: # skip TIFF header on subsequent pages # tiff header -- PIL always starts the first IFD at offset 8 fp.write(self._prefix + self._pack("HL", 42, 8)) @@ -1059,7 +1062,6 @@ ImageFileDirectory = ImageFileDirectory_v1 class TiffImageFile(ImageFile.ImageFile): - format = "TIFF" format_description = "Adobe TIFF" _close_exclusive_fp_after_loading = False @@ -1582,7 +1584,6 @@ SAVE_INFO = { def _save(im, fp, filename): - try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: @@ -1847,13 +1848,18 @@ def _save(im, fp, filename): e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - l, s, d = e.encode(16 * 1024) + errcode, data = e.encode(16 * 1024)[1:] if not _fp: - fp.write(d) - if s: + fp.write(data) + if errcode: break - if s < 0: - msg = f"encoder error {s} when writing image file" + if _fp: + try: + os.close(_fp) + except OSError: + pass + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) else: diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 9b5277138..ac048ba56 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -312,7 +312,7 @@ TAGS = { 34910: "HylaFAX FaxRecvTime", 36864: "ExifVersion", 36867: "DateTimeOriginal", - 36868: "DateTImeDigitized", + 36868: "DateTimeDigitized", 37121: "ComponentsConfiguration", 37122: "CompressedBitsPerPixel", 37724: "ImageSourceData", diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index 0dc695a88..e4f47aa04 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -28,7 +28,6 @@ from ._binary import i32le as i32 class WalImageFile(ImageFile.ImageFile): - format = "WAL" format_description = "Quake2 Texture" diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 1d074f78c..d060dd4b8 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -35,7 +35,6 @@ def _accept(prefix): class WebPImageFile(ImageFile.ImageFile): - format = "WEBP" format_description = "WebP image" __loaded = 0 diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 639730b8e..0ecab56a8 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -75,7 +75,6 @@ def _accept(prefix): class WmfStubImageFile(ImageFile.StubImageFile): - format = "WMF" format_description = "Windows Metafile" @@ -86,7 +85,6 @@ class WmfStubImageFile(ImageFile.StubImageFile): s = self.fp.read(80) if s[:6] == b"\xd7\xcd\xc6\x9a\x00\x00": - # placeable windows metafile # get units per inch diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index f0e05e867..aa4a01f4e 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -41,12 +41,10 @@ def _accept(prefix): class XVThumbImageFile(ImageFile.ImageFile): - format = "XVThumb" format_description = "XV thumbnail image" def _open(self): - # check magic if not _accept(self.fp.read(6)): msg = "not an XV thumbnail file" diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index ad18e0031..3c12564c9 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -44,12 +44,10 @@ def _accept(prefix): class XbmImageFile(ImageFile.ImageFile): - format = "XBM" format_description = "X11 Bitmap" def _open(self): - m = xbm_head.match(self.fp.read(512)) if not m: @@ -69,7 +67,6 @@ class XbmImageFile(ImageFile.ImageFile): def _save(im, fp, filename): - if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 5fae4cd68..5d5bdc3ed 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class XpmImageFile(ImageFile.ImageFile): - format = "XPM" format_description = "X11 Pixel Map" def _open(self): - if not _accept(self.fp.read(9)): msg = "not an XPM file" raise SyntaxError(msg) @@ -68,7 +66,6 @@ class XpmImageFile(ImageFile.ImageFile): palette = [b"\0\0\0"] * 256 for _ in range(pal): - s = self.fp.readline() if s[-2:] == b"\r\n": s = s[:-2] @@ -79,9 +76,7 @@ class XpmImageFile(ImageFile.ImageFile): s = s[2:-2].split() for i in range(0, len(s), 2): - if s[i] == b"c": - # process colour key rgb = s[i + 1] if rgb == b"None": @@ -99,7 +94,6 @@ class XpmImageFile(ImageFile.ImageFile): break else: - # missing colour key msg = "cannot read this XPM file" raise ValueError(msg) @@ -110,7 +104,6 @@ class XpmImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] def load_read(self, bytes): - # # load all image data in one chunk diff --git a/src/_imaging.c b/src/_imaging.c index 05e1370f6..1c25ab00c 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -251,6 +251,10 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { static const char *must_be_sequence = "argument must be a sequence"; static const char *must_be_two_coordinates = "coordinate list must contain exactly 2 coordinates"; +static const char *incorrectly_ordered_x_coordinate = + "x1 must be greater than or equal to x0"; +static const char *incorrectly_ordered_y_coordinate = + "y1 must be greater than or equal to y0"; static const char *wrong_mode = "unrecognized image mode"; static const char *wrong_raw_mode = "unrecognized raw mode"; static const char *outside_image = "image index out of range"; @@ -2805,6 +2809,16 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawArc( self->image->image, @@ -2886,6 +2900,16 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawChord( self->image->image, @@ -2932,6 +2956,16 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawEllipse( self->image->image, @@ -3101,6 +3135,16 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawPieslice( self->image->image, @@ -3197,6 +3241,16 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); + free(xy); + return NULL; + } n = ImagingDrawRectangle( self->image->image, @@ -3984,8 +4038,6 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args); extern PyObject * PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args); extern PyObject * -PyImaging_ListWindowsWin32(PyObject *self, PyObject *args); -extern PyObject * PyImaging_EventLoopWin32(PyObject *self, PyObject *args); extern PyObject * PyImaging_DrawWmf(PyObject *self, PyObject *args); @@ -4069,7 +4121,6 @@ static PyMethodDef functions[] = { {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, METH_VARARGS}, {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, METH_VARARGS}, {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, METH_VARARGS}, - {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, METH_VARARGS}, {"drawwmf", (PyCFunction)PyImaging_DrawWmf, METH_VARARGS}, #endif #ifdef HAVE_XCB diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 9b5a121d7..efb045667 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -116,7 +116,7 @@ cms_profile_open(PyObject *self, PyObject *args) { } static PyObject * -cms_profile_fromstring(PyObject *self, PyObject *args) { +cms_profile_frombytes(PyObject *self, PyObject *args) { cmsHPROFILE hProfile; char *pProfile; @@ -960,8 +960,7 @@ _is_intent_supported(CmsProfileObject *self, int clut) { static PyMethodDef pyCMSdll_methods[] = { {"profile_open", cms_profile_open, METH_VARARGS}, - {"profile_frombytes", cms_profile_fromstring, METH_VARARGS}, - {"profile_fromstring", cms_profile_fromstring, METH_VARARGS}, + {"profile_frombytes", cms_profile_frombytes, METH_VARARGS}, {"profile_tobytes", cms_profile_tobytes, METH_VARARGS}, /* profile and transform functions */ diff --git a/src/_webp.c b/src/_webp.c index 493e0709c..e8d01f7b2 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -955,6 +955,13 @@ addTransparencyFlagToModule(PyObject *m) { static int setup_module(PyObject *m) { +#ifdef HAVE_WEBPANIM + /* Ready object types */ + if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || + PyType_Ready(&WebPAnimEncoder_Type) < 0) { + return -1; + } +#endif PyObject *d = PyModule_GetDict(m); addMuxFlagToModule(m); addAnimFlagToModule(m); @@ -963,13 +970,6 @@ setup_module(PyObject *m) { PyDict_SetItemString( d, "webpdecoder_version", PyUnicode_FromString(WebPDecoderVersion_str())); -#ifdef HAVE_WEBPANIM - /* Ready object types */ - if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || - PyType_Ready(&WebPAnimEncoder_Type) < 0) { - return -1; - } -#endif return 0; } diff --git a/src/decode.c b/src/decode.c index 7a9b956c5..7e3fadc04 100644 --- a/src/decode.c +++ b/src/decode.c @@ -116,12 +116,11 @@ _dealloc(ImagingDecoderObject *decoder) { static PyObject * _decode(ImagingDecoderObject *decoder, PyObject *args) { - UINT8 *buffer; - Py_ssize_t bufsize; + Py_buffer buffer; int status; ImagingSectionCookie cookie; - if (!PyArg_ParseTuple(args, "y#", &buffer, &bufsize)) { + if (!PyArg_ParseTuple(args, "y*", &buffer)) { return NULL; } @@ -129,12 +128,13 @@ _decode(ImagingDecoderObject *decoder, PyObject *args) { ImagingSectionEnter(&cookie); } - status = decoder->decode(decoder->im, &decoder->state, buffer, bufsize); + status = decoder->decode(decoder->im, &decoder->state, buffer.buf, buffer.len); if (!decoder->pulls_fd) { ImagingSectionLeave(&cookie); } + PyBuffer_Release(&buffer); return Py_BuildValue("ii", status, decoder->state.errcode); } diff --git a/src/display.c b/src/display.c index 0ce10e249..e8e7b62c2 100644 --- a/src/display.c +++ b/src/display.c @@ -195,20 +195,21 @@ _releasedc(ImagingDisplayObject *display, PyObject *args) { static PyObject * _frombytes(ImagingDisplayObject *display, PyObject *args) { - char *ptr; - Py_ssize_t bytes; + Py_buffer buffer; - if (!PyArg_ParseTuple(args, "y#:frombytes", &ptr, &bytes)) { + if (!PyArg_ParseTuple(args, "y*:frombytes", &buffer)) { return NULL; } - if (display->dib->ysize * display->dib->linesize != bytes) { + if (display->dib->ysize * display->dib->linesize != buffer.len) { + PyBuffer_Release(&buffer); PyErr_SetString(PyExc_ValueError, "wrong size"); return NULL; } - memcpy(display->dib->bits, ptr, bytes); + memcpy(display->dib->bits, buffer.buf, buffer.len); + PyBuffer_Release(&buffer); Py_INCREF(Py_None); return Py_None; } @@ -421,79 +422,6 @@ error: return NULL; } -static BOOL CALLBACK -list_windows_callback(HWND hwnd, LPARAM lParam) { - PyObject *window_list = (PyObject *)lParam; - PyObject *item; - PyObject *title; - RECT inner, outer; - int title_size; - int status; - - /* get window title */ - title_size = GetWindowTextLength(hwnd); - if (title_size > 0) { - title = PyUnicode_FromStringAndSize(NULL, title_size); - if (title) { - GetWindowTextW(hwnd, PyUnicode_AS_UNICODE(title), title_size + 1); - } - } else { - title = PyUnicode_FromString(""); - } - if (!title) { - return 0; - } - - /* get bounding boxes */ - GetClientRect(hwnd, &inner); - GetWindowRect(hwnd, &outer); - - item = Py_BuildValue( - F_HANDLE "N(iiii)(iiii)", - hwnd, - title, - inner.left, - inner.top, - inner.right, - inner.bottom, - outer.left, - outer.top, - outer.right, - outer.bottom); - if (!item) { - return 0; - } - - status = PyList_Append(window_list, item); - - Py_DECREF(item); - - if (status < 0) { - return 0; - } - - return 1; -} - -PyObject * -PyImaging_ListWindowsWin32(PyObject *self, PyObject *args) { - PyObject *window_list; - - window_list = PyList_New(0); - if (!window_list) { - return NULL; - } - - EnumWindows(list_windows_callback, (LPARAM)window_list); - - if (PyErr_Occurred()) { - Py_DECREF(window_list); - return NULL; - } - - return window_list; -} - /* -------------------------------------------------------------------- */ /* Windows clipboard grabber */ diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 77343e583..82f290bd0 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -85,25 +85,22 @@ point32(Imaging im, int x, int y, int ink) { static inline void point32rgba(Imaging im, int x, int y, int ink) { - unsigned int tmp1; + unsigned int tmp; if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { UINT8 *out = (UINT8 *)im->image[y] + x * 4; UINT8 *in = (UINT8 *)&ink; - out[0] = BLEND(in[3], out[0], in[0], tmp1); - out[1] = BLEND(in[3], out[1], in[1], tmp1); - out[2] = BLEND(in[3], out[2], in[2], tmp1); + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); } } static inline void hline8(Imaging im, int x0, int y0, int x1, int ink) { - int tmp, pixelwidth; + int pixelwidth; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -126,13 +123,9 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) { static inline void hline32(Imaging im, int x0, int y0, int x1, int ink) { - int tmp; INT32 *p; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -152,13 +145,9 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) { static inline void hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { - int tmp; - unsigned int tmp1; + unsigned int tmp; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -173,9 +162,9 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4; UINT8 *in = (UINT8 *)&ink; while (x0 <= x1) { - out[0] = BLEND(in[3], out[0], in[0], tmp1); - out[1] = BLEND(in[3], out[1], in[1], tmp1); - out[2] = BLEND(in[3], out[2], in[2], tmp1); + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); x0++; out += 4; } diff --git a/winbuild/build.rst b/winbuild/build.rst index 716669771..d4275a274 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -96,9 +96,7 @@ directory. Example ------- -The following is a simplified version of the script used on AppVeyor: - -.. code-block:: +The following is a simplified version of the script used on AppVeyor:: set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 89903c621..2820bdb36 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -109,9 +109,9 @@ header = [ deps = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/2.1.5/libjpeg-turbo-2.1.5.tar.gz/download", - "filename": "libjpeg-turbo-2.1.5.tar.gz", - "dir": "libjpeg-turbo-2.1.5", + + "/libjpeg-turbo/files/2.1.5.1/libjpeg-turbo-2.1.5.1.tar.gz/download", + "filename": "libjpeg-turbo-2.1.5.1.tar.gz", + "dir": "libjpeg-turbo-2.1.5.1", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" @@ -253,9 +253,9 @@ deps = { "libs": ["*.lib"], }, "freetype": { - "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.gz", # noqa: E501 - "filename": "freetype-2.12.1.tar.gz", - "dir": "freetype-2.12.1", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.0.tar.gz", # noqa: E501 + "filename": "freetype-2.13.0.tar.gz", + "dir": "freetype-2.13.0", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { @@ -289,9 +289,9 @@ deps = { # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { - "url": SF_PROJECTS + "/lcms/files/lcms/2.14/lcms2-2.14.tar.gz/download", - "filename": "lcms2-2.14.tar.gz", - "dir": "lcms2-2.14", + "url": SF_PROJECTS + "/lcms/files/lcms/2.15/lcms2-2.15.tar.gz/download", + "filename": "lcms2-2.15.tar.gz", + "dir": "lcms2-2.15", "license": "COPYING", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { @@ -356,9 +356,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/6.0.0.zip", - "filename": "harfbuzz-6.0.0.zip", - "dir": "harfbuzz-6.0.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.1.0.zip", + "filename": "harfbuzz-7.1.0.zip", + "dir": "harfbuzz-7.1.0", "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"),